infuse.host
Infuse your HTML with dynamic content.
Introduction
Infuse.host allows you to infuse HTML templates with dynamic content. The resulting infused HTML fragments can then be added to host elements. This is done by writing template literals or expressions in your HTML templates. It also allows you to:
- Write event handlers, the same way you would normally write them (using on-event attributes), but with access to the
host
and other variables. - Write watches to automatically re-infuse an element when an event occurs on another element.
- Write iterating templates to infuse a template iteratively, based on values in a given iterable variable.
Installation
You can install the infuse.host NPM package in your project by running the following command.
Webpack
In order to keep things simple, all the code examples shown below have templates that are parsed in the browser (using the parseTemplate
function). However, parsing templates in the browser is a practice that is discouraged in a production environment. You can avoid parsing templates in the browser by parsing them as part of a build or back-end process.
If you're using webpack, you can use infuse-loader to parse templates in HTML files and generate ES modules. These modules can then be imported into other modules. You can install the infuse-loader NPM package in your project by running the following command.
The site https://todo.infuse.host/ is an example of a web application built using infuse.host, in which all templates are parsed as part of a webpack build process using infuse-loader. The source code can be found at https://github.com/serg-io/todo.infuse.host.
Examples
In the following "Hello World" example, the <template>
element is parsed, cloned, and infused and the resulting fragment (<h1>Hello World</h1>
) is added to <header>
(the host
element).
Loading example...
The same result can be achieved using a custom element. The Infuse.Host
class can be extended to define custom elements. In this example the text is obtained from the custom element (host.title
is used in the template instead of data.title
).
Loading example...
In the previous example the infused fragment is appended to the <custom-header>
using regular DOM. However, a Shadow DOM can be used by setting the element's shadowRootMode
to either 'open'
or 'closed'
. In the following example the <button>
element is extended to define a customized built-in element that uses a shadow DOM.
Loading example...
The following is a more elaborate example that simulates a shopping cart.
Loading example...
Template Literals
You can write template literals in your HTML code (inside elements and attribute values), the same way you would write them in regular Javascript code. They start and end with back-tick characters, they can contain strings and expressions, and they can be tagged. However, in order to be able to tag template literals, you must tell infuse.host what are the tag functions that you use within your HTML templates. You do this by setting the tags
configuration option.
A common use case for tagged template literals is internationalization. For instance, if you want to tag your template literals with a tag function called i18n
you must use the configs ES module to set the tags
configuration option:
import { setConfigs } from 'path/to/infuse.host/src/configs.js';
const dictionary = { submit: 'Submit' };
/**
* Set the "tags" configuration option. Must be set before parsing.
* The `i18n` tag function retrieves the value of the given `key` from the `dictionary`.
*/
setConfigs({
tags: {
i18n: function([key]) {
return dictionary[key];
}
}
});
The parser uses the "tags" configuration option to identify the tag functions that you use in your HTML templates. For instance, if you use the tags
object shown above, i18n`submit`
will be infused with (replaced with) the string "Submit".
Loading example...
Expressions
An expression starts with ${
and ends with }
and contains Javascript code, which is evaluated during the infuse process, and its result is used to infuse the corresponding part of an element. Expressions can be used inside template literals, inside back-tick characters (just like in regular Javascript code), or directly in your HTML code, without back-tick characters.
The following variables are available within expressions:
this
: The element that contains the expression.host
: The element to which the infused fragment will be added.data
: An optional data object.tags
: The configuration object that contains all tag functions.event
: When an element is infused by a watch, theevent
variable is the event that triggered the infusion. This variable isundefined
the first time an element is infused.- Elements can define their own custom constant variables.
- Additional iteration constant variables are available within iterating templates.
Expressions and template literals can be used in combination with static strings. For instance:
Custom Constants
Custom constants are variables available within the expressions and event handlers of an element. These constants are defined using attributes that follow this format: const-variable-name="${ expression }"
, where variable-name
is the name of the variable, expression
is the Javascript code that gets evaluated (right before the element is infused for the first time), and the resulting value is assigned to the variable.
For instance, lets say that host.getCarInventory()
returns an array of objects, each object represents a car with a color
attribute. The following paragraph displays how many blue cars there are in the inventory:
<p const-car-inventory="${ host.getCarInventory() }">
Number of blue cars: ${ carInventory.filter(car => car.color === 'blue').length }
</p>
Note: In HTML, attributes names are case-insensitive (they're lower-cased automatically). To define a variable with uppercase letters in its name, you must use dashes as shown in the previous example.
Note: the event
variable is not available in expressions to declare custom constants.
Parts
There are four parts of an element that can be infused:
- Attributes
- Boolean attributes
- Properties
- Text child nodes
Attributes
In the following example, the class
attribute will be infused with "is-valid" (in addition to "form-control") if host.isEmailValid
evaluates to a truthy value:
Boolean Attributes
Boolean attributes have names that end with a question mark and they're added to the element if their expressions result in a truthy value.
If the result of an expression is true
, the attribute is added to the element using an empty string as its value. If the result is a truthy value, other than true
, it will be used as the value of the attribute. Otherwise, if the expression results in a falsy value, the attribute is removed from the element.
In the following example, the attribute disabled
is added to the <button>
element if host.isFormInvalid
is true
:
When an expressions results in a string that is not empty, the string will be used as the value of the attribute. For instance, if host.getBtnClass()
returns the string "btn btn-warning"
, it will be used as the value of the class
attribute. However, if it returns a falsy value, the class
attribute will not be added to the button at all.
Properties
Properties of elements can be infused by adding an attribute in the HTML code that starts with a dot. The expression given in the value will be evaluated and the result will be assigned to the specified property of the element. In the following example, the expression evaluates to a Date
instance which is assigned to the property valueAsDate
of the input element:
Note: In HTML, attributes names are case-insensitive (they're lower-cased automatically). To infuse a property with uppercase letters in its name use dashes as shown in the example above.
Text Child Nodes
The text child nodes of an element can be infused by adding an expression or a template literal:
Tagged template literals can be used for things like internationalization:
Event Handlers
You can add event handlers to elements, just like you would in regular HTML code, by using on-event attributes.
When an element is parsed, event handler attributes are converted into event listener functions (terminology). When an element is infused for the first time, these event listeners are added to the element using the addEventListener
method.
Event handlers have access to the same variables available within expressions. For instance, if you want to call a method on the host
element when a form is submitted, you would add an onsubmit
event handler attribute.
Loading example...
Watches
Watches re-infuse an element when an event occurs on itself or another element.
For instance, when the following button is infused for the first time the disabled
boolean attribute will not be added because the event
variable is undefined
.
A watch can be added to infuse the button again whenever it is clicked. When a watch infuses an element, the value of event
is the event that triggered the infusion.
In the following example, the watch will listen for the click
event on the button itself (this
). When the event occurs, the element will be re-infused, this time the event
will be the click
event that triggered the watch, which will cause the disabled
attribute to be added to the button.
<button type="submit" disabled?="${ event !== undefined }" watch-this="click">
i18n`submit`
</button>
Watches can also be used to watch events on other elements. For instance, the button can be infused when a submit
event occurs on the host
element:
<button type="submit" disabled?="${ event !== undefined }" watch-host="submit">
i18n`submit`
</button>
If the host
element in the previous example contains multiple forms, the button will be disabled whenever any of those forms are submitted. This is because events bubble up the DOM and eventually reach the host
.
Watches can be limited to infuse an element only when the specified type of event is triggered by an element that matches a given selector. For instance, the following button will be disabled when a submit
event reaches the host
element and the event was triggered by a form with "login-form" as its ID:
<button type="submit" disabled?="${ event !== undefined }" watch-host="submit #login-form">
i18n`login`
</button>
Watches are written in this format: watch-variable="event-map"
, where variable
is the name of the variable (element) to watch. This is usually host
or this
, but it can be any variable available within the element's expressions as long as it's an Element
or implements the addEventListener
method.
The event-map
can be written in any of the following formats:
- A string,
watch-variable="eventList"
, where:eventList
is a list of semicolon-separatedevent
s:event1[; event2]...[; eventN]
.event
is aneventType
optionally followed by a comma-separated list ofselector
s:eventType[ selector1[, selector2]...[, selectorN] ]
eventType
is the type of event to watch, for instanceclick
orsubmit
.selector
is an optional CSS selector used for event delegation. A watch will only infuse an element if the element that triggered the event matches theselector
.
For instance, the following button will be infused only when a form triggers a
submit
event or a form field with therequired
boolean attribute triggers aninvalid
event: - An object,
watch-variable="{ eventList: parts }"
, where:- The keys follow the
eventList
structure explained above. - The values indicate one (a string or a number) or multiple (an array) parts of the element to infuse.
- If the part is a boolean attribute, it must end with a question mark (i.e.
disabled?
). - If the part is a property, it must start with a dot and it can be either dashed (i.e.
.value-as-date
) or camel cased (i.e..valueAsDate
). If the part is a text child node, it must be a number indicating the zero-based index position of the text child node to infuse. For instance, consider the following
It contains three child nodes:<p>
element:- A text child node (
i18n`submit`
). - An element child node (
<strong>=</strong>
). - A text child node (
${ host.getTotal() }
).
- A text child node (
- If the part is a boolean attribute, it must end with a question mark (i.e.
In the following example, when a
submit
event reaches thehost
, the watch will only infuse thedisabled
boolean attribute. - The keys follow the
An array of arrays,
watch-variable="[ [eventList, parts] ]"
. This is the same as using an object, but theeventList
/parts
pairs are written as arrays. For instance, the watch in the previous example can be written as an array of arrays and the watch would work the same way.An expression,
watch-variable="${ expression }"
. The expression, when evaluated, must return a value in one of the formats listed above (a string, an object, or an array of arrays).In the following example,
host.getEventMapForSubmitBtn()
must return a string, an object, or an array of arrays:
Iterating Templates
Iterating templates use the for
and each
attributes to indicate that the template must be cloned and infused multiple times. The value of the for
attribute is a string with up to 3 comma-separated variable names to use in each iteration. The value of the each
attribute must be an expression. Evaluating the expression must result in a collection (array, Map
, Set
, or any value that has a forEach
method). The collection is used to clone the template multiple times, once for each value in the collection.
Using the for
and each
attributes to clone and infuse a template multiple times is analogous to using the forEach
method to iterate over multiple values in regular Javascript. For instance, in Javascript you would write a forEach
loop like this:
host.getBooksArray().forEach(function(book, index, books) {
// Code to execute in each iteration.
// The variables `book`, `index`, and `books` are available within each iteration.
});
A similar iteration can be implemented to clone and infuse a template multiple times by adding the for
and each
attributes to the <template>
element:
<template for="book, index, books" each="${ host.getBooksArray() }">
<!--
Contents of this template will be cloned and infuse once for each `book`.
The variables `book`, `index`, and `books` are available within any expression and
event handlers inside this template.
-->
</template>
Lets say that host.getBooksArray()
returns an array of objects, each object represents a book and contains the attributes isbn
, title
, and author
. The following template can be used to generate a <table>
with a list of books.
Loading example...
Note how the iterating template is inside a parent template. When a template is cloned and infused, nested templates are also cloned and infused.
Custom Elements
The Infuse.Host
class can be extended to define a class for a custom element.
Defining a template
property is the only requirement when extending Infuse.Host
. The purpose of the template
property is to provide the template element that will be cloned and infused whenever the custom element is used.
By default, the infused fragment will be appended to the custom element's Light DOM. However, a Shadow DOM can be used by setting the shadowRootMode
property to either 'open'
or 'close'
(which are the two modes that the attachShadow
method accepts).
Both, the template
and shadowRootMode
properties, can be defined as getters or regular functions.
Lets say you want to define a custom element for a login form, that uses a Shadow DOM, and that you're using webpack and infuse-loader to parse and import your templates. The following code would define a custom element called <login-form>
.
import * as Infuse from 'path/to/infuse.host/src/infuse.js';
import loginTemplate from './login-form.html';
// Extend `Infuse.Host` to define a class for the new custom element.
class LoginForm extends Infuse.Host {
get template() {
// Return the parsed template.
return loginTemplate;
}
get shadowRootMode() {
// Use an "open" shadow root.
return 'open';
}
}
// Define the custom element.
window.customElements.define('login-form', LoginForm);
When the <login-form>
custom element is used, Infuse.Host
uses the template
property to obtain the template that needs to be cloned and infused. And, since shadowRootMode
is set to 'open'
, the resulting fragment is added to the custom element's Shadow DOM. When the element is removed from the DOM, memory allocated for the element, and any of its descendants, is cleared automatically, there's no need to call the clear
function.
Customized Built-in Elements
If you want to extend one of the browser's built-in elements you can use the CustomHost
function to define a customized built-in element.
For instance, lets say that your application contains a shopping cart and you want to summarize the items in the cart using a list (an <ul>
element) where each element in the list is an item in the cart. You can define a customized built-in element that extends the HTMLLIElement
class (the <li>
element):
import { CustomHost } from 'path/to/infuse.host/src/infuse.js';
import cartItemTemplate from './cart-item.html';
/**
* Extend the `HTMLLIElement` class to define a new `CartItem` class.
*/
class CartItem extends CustomHost(HTMLLIElement) {
get template() {
// Return the parsed template.
return cartItemTemplate;
}
}
/**
* When defining customized built-in elements, you must specify (in the third argument) what
* built-in HTML tag the custom element extends.
*/
window.customElements.define('cart-item', CartItem, { extends: 'li' });
Once defined, you can create them manually using the constructor:
or using document.createElement
:
and add them to the DOM manually.
You can also use them in HTML templates:
Cleanup
When a template is cloned and the cloned fragment is infused, memory is allocated to create variables, event listeners, and watches. Infuse.host keeps track of allocated memory. When an element is removed from the DOM and is no longer needed, memory associated with that element must be cleared. This memory can be cleared using the clear
function. The clear
function searches for infused elements and clears memory associated with the elements it finds.
In the following example a <form>
element is removed from the DOM and the clear
function is called using the form
as the only argument. The clear
function will search for infused elements inside the form
and clear memory that was allocated to infuse them. If the form itself is an infused element, memory associated with the form will also be cleared.
import { clear } from 'path/to/infuse.host/src/infuse.js';
const form = document.querySelector('#login-form');
// Remove form from the DOM.
form.remove();
// Clear memory from any infused element within the form (including the form itself).
clear(form);
Note: Custom elements defined using the Infuse.Host
class or the Infuse.CustomHost
function call the clear
method when the custom element is removed from the DOM (when the disconnectedCallback
method is called). Therefore, the clear
function doesn't need to be called for custom elements defined using Infuse.Host
or Infuse.CustomHost
.
Command Line Interface
The infuse-cli package provides a command line interface (CLI) to convert HTML templates (files) to ES Modules which can be used with infuse.host. To install it execute the following command:
Once installed you can use the infuse
command in your NPM scripts or by using it with NPM's npx
command. For instance, the following command will parse templates in the src/template.html file and will write the generated ES Module to dist/template.js.
Documentation and additional examples can be found in the infuse-cli package web page. You can also use the CLI to access the documentation:
Configuration Options
The config ES module allows you to change configuration options.
In the following example, the setConfigs
function is used to change the configuration options eventHandlerExp
and eventName
:
import { setConfigs } from 'path/to/infuse.host/src/configs.js';
setConfigs({
// Change the name of the "event" variable to just "e".
eventName: 'e',
// Change the prefix of event handler attributes to "on-".
eventHandlerExp: 'on-'
});
The following is a list of all the configuration options:
camelCaseEvents
: Indicates whether or not event names in event handlers should be camel cased during the parsing process. This option isfalse
by default.Consider the following element:
By default, this event handler will be listening for
'client-side-validation'
events. However, ifcamelCaseEvents
is set totrue
, the event handler would be listening for'clientSideValidation'
events instead.constantExp
: The prefix (a string) or a regular expression used to determine if an attribute is a custom constant. This is'const-'
by default which means that all attributes that start withconst-
are custom constant definitions.If
constantExp
is set to a regular expression, the regular expression is used to determine if attributes are custom constants. For instance, if you want custom constant attribute names to end with-const
you would use the following regular expression:If you're using a regular expression, it must contain parenthesis, as shown above. The parenthesis indicate the location of the name of the variable.
contextFunctionId
: When an element is parsed a context function is generated. An ID that uniquely identifies the context function is also created. This ID is added to the element as an attribute. ThecontextFunctionId
option defines the name of that attribute. This is'data-cid'
by default.eventHandlerExp
: The prefix (a string) or a regular expression used to determine if an attribute is an event handler. By default, event handler attributes start withon
oron-
and can contain alphanumeric characters, underscores, dashes, and colons. The following regular expression is used by default to identify event handler attributes:Parentheses are required when using a regular expression, they indicate the location of the event type. For instance, when using the following expression:
you would write event handlers using the format
when-eventtype-run="expression"
, whereeventtype
is the type of event andexpression
is the expression to execute when the event occurs. For example, when usingwhen-submit-run="event.preventDefault()"
the event handler would listen forsubmit
events.Alternatively, you can use a string to indicate the prefix of event handlers. For instance, when using
'on'
:all attributes that start with
on
will be treated as event handlers.eventName
: Name of the event variable available within expressions and event handlers. This is'event'
by default. If you wanted event variables to be namede
instead, you would have to change this configuration option:This would allow you to use
e
(instead ofevent
) to access event variables within expressions and event handlers. For instance:placeholderId
: Nested templates are replaced with placeholder templates during the parsing process. Placeholder templates have an attribute that uniquely identifies the template that was replaced. TheplaceholderId
defines the name of the attribute. This is'data-pid'
by default.sweepFlag
: Name of the boolean attribute to use as an indicator that memory has been allocated for an element. Allocated memory must be cleared when the element is removed from the DOM. This is'data-sweep'
by default.tags
: An object containing all possible tag functions to be used with template literals. During parsing (for instance, when using the infuse-loader) this can be an array (of strings) of all possible tag function names. However, at run time, this must be an object where the keys are the names of the tag functions and the values are the tag functions.tagsName
: Name of the variable, available within expressions and event handlers, that contains all tag functions. This is'tags'
by default.templateId
: Name of the template ID attribute. After parsing a template, a template ID attribute is automatically generated and added to the template element. This configuration option is the name of that attribute and it's'data-tid'
by default.If the template element already has this attribute when it's parsed, a new ID is not generated, instead the value of the attribute is used to identify the template.
If you're already setting the
id
attribute on your templates, changing this configuration option toid
would make it easier to access your templates (especially if you're using infuse-loader, see infuse-loader for details).watchExp
: The prefix (a string) or a regular expression used to determine if an attribute is a watch. This is'watch-'
by default which means that all attributes that start withwatch-
are treated as watches.If
watchExp
is set to a regular expression, the regular expression is used to determine if attributes are watches. For instance, if you want watch attribute names to end with-watch
you would use the following regular expression:If you're using a regular expression, it must contain parenthesis, as shown above. The parenthesis indicate the location of the name of the variable/element to watch.
License
MIT.