Incremental DOM is a library for expressing and applying updates to DOM trees. JavaScript can be used to extract, iterate over and transform data into calls generating HTMLElements and Text nodes. It differs from Virtual DOM approaches in that a diff operation is performed incrementally (that is one node at a time) against the DOM, rather than on a virtual DOM tree.
Rather than targeting direct usage, Incremental DOM aims to provide a platform for higher level libraries or frameworks. As you might notice from the examples, Incremental DOM-style markup can be somewhat challenging to write and read. See Why Incremental DOM for an explanation.
The DOM to be rendered is described with the incremental node functions, elementOpen, elementClose and text. For example, the following function:
function renderPart() {
elementOpen('div');
text('Hello world');
elementClose('div');
}
would correspond to
<div>
Hello world
</div>
Using the renderPart function from above, the patch function can be used to render the desired structure into an existing Element or Document (which includes Shadow DOM). Calling the patch function again will patch the DOM tree with any changes, updating attributes, and creating/removing DOM nodes as needed.
In addition to creating DOM nodes, you can also add/update attributes and properties on Elements. They are specified as variable arguments, alternating between attribute/property name and value. Values that are Objects or Functions are set as properties, with all others being set as attributes.
One use for setting a property could be to store callbacks for use with an event delegation library. Since you can assign to any property on the DOM node, you can even assign to on* handlers, like onclick.
Often times, you know that some properties on a DOM node will not change. One example would be the type attribute in <input type="text">. Incremental DOM provides a shortcut to avoid comparing attributes/properties you know will not change. The third argument to elementOpen is an array of unchanging attributes. To avoid allocating an array on each pass, you will want to declare the array in a scope that is only executed once.
If the statics array is provided, you must also provide a key. This ensures that an Element with the same tag but different statics array is never re-used by Incremental DOM.
Styles for an element can be set either using a string or an object. When setting styles using an object, the names should be camelCase as they are set on the Element's style property.
As you can mix node declarations and JavaScript, rendering conditional branches is fairly straightforward. Simply place the node declarations inside a branch. This works with switch statements too!
You can use your favorite way to render an array (or any sort of iterable) of items. When rendering an array of items, you will want to specify a 'key' as the second argument to the elementOpen function. Incremental DOM uses the key in order to:
Prevent the treating of newly added or moved items as a diff that needs to be reconciled.
Correctly maintain focus on any input fields, buttons or other items that may receive focus that have moved.
As Incremental DOM does not know when you are rendering an array of items, there is no warning generated when a key should be specified but is not present. If you are compiling from a template or transpiling, it might be a good idea to statically check to make sure a key is specified.
Incremental DOM provides some helpers to give some additional control over how attribures are specified. The elementOpenStart, attr and elementOpenEnd functions act as a helper for calling elementOpen, allowing you to mix logic and attributes or call other functions.
elementOpenStart('div');
for (var key in obj) {
attr(key, obj[key]);
}
elementOpenEnd('div');
Passing Functions
The incremental node functions are evaluated when they are called. If you do not want to have them appear in the current location (e.g. to pass them to another function), simply wrap the statements in a function which can be called later.
function renderStatement(content, isStrong) {
if (isStrong) {
elementOpen('strong');
content();
elementClose('strong');
} else {
content();
}
}
function renderGreeting(name) {
function content() {
text('Hello ');
text(name);
}
elementOpen('div');
renderStatement(content, true);
elementClose('div');
}
Hooks
Setting Values
Incremental DOM provides hooks to allow customization of how values are set. The attributes object allows you to provide a function to decide what to do when an attribute passed to elementOpen or similar functions changes. The following example makes Incremental DOM always set value as a property.
If no function is specified for a given name, a default function is used that applies values as described in Attributes and Properties. This can be changed by specifying the function for symbols.default.
import {
attributes,
symbols
} from 'incremental-dom';
attributes[symbols.default] = someFunction;
Added/Removed Nodes
You can be notified when Nodes are added or removed by Incremental DOM by specifying functions for notifications.nodesCreated and notifications.nodesDeleted. If there are added or removed nodes during a patch operation, the appropriate function will be called at the end of the patch with the added or removed nodes.
import { notifications } from 'incremental-dom';
notifications.nodesCreated = function(nodes) {
nodes.forEach(function(node) {
// node may be an Element or a Text
});
};
API
elementOpen
Description
Declares an Element with zero or more attributes/properties that should be present at the current location in the document tree.
Parameters
string tagname
The name of the tag, e.g. 'div' or 'span'. This could also be the tag of a custom element.
string key
The key that identifies Element for reuse. See Arrays of Items
Array staticPropertyValuePairs
Pairs of property names and values. Depending on the type of the value, these will be set as either attributes or properties on the Element. These are only set on the Element once during creation. These will not be updated during subsequent passes. See Statics Array.
vargs propertyValuePairs
Pairs of property names and values. Depending on the type of the value, these will be set as either attributes or properties on the Element.
The name of the tag, e.g. 'div' or 'span'. This could also be the tag of a custom element.
string key
The key that identifies Element for reuse. See Arrays of Items
Array staticPropertyValuePairs
Pairs of property names and values. Depending on the type of the value, these will be set as either attributes or properties on the Element. These are only set on the Element once during creation. These will not be updated during subsequent passes. See Statics Array.
Signifies the end of the element opened with elementOpen, corresponding to a closing tag (e.g. </div> in HTML). Any childNodes of the currently open Element that are in the DOM that have not been encountered in the current render pass are removed by the call to elementClose.
Parameters
string tagname
The name of the tag, e.g. 'div' or 'span'. This could also be the tag of a custom element.
Returns
Element The corresponding DOM Element.
Usage
import { elementClose } from 'incremental-dom';
…
elementClose('div');
The name of the tag, e.g. 'div' or 'span'. This could also be the tag of a custom element.
string key
The key that identifies Element for reuse. See Arrays of Items
Array staticPropertyValuePairs
Pairs of property names and values. Depending on the type of the value, these will be set as either attributes or properties on the Element. These are only set on the Element once during creation. These will not be updated during subsequent passes. See Statics Array.
vargs propertyValuePairs
Pairs of property names and values. Depending on the type of the value, these will be set as either attributes or properties on the Element.
Declares a Text node, with the specified text, should appear at the current location in the document tree.
Parameters
string|boolean|number value
The value for the Text node.
...function formatters
Optional functions that format the value when it changes.
Returns
Text The corresponding DOM Text Node.
Usage
import { text } from 'incremental-dom';
function toUpperCase(str) {
return str.toUpperCase();
}
…
text('hello world', toUpperCase);
patch
Description
Updates the provided Node with a function containing zero or more calls to elementOpen, text and elementClose. The provided callback function may call other such functions. The patch function may be called with a new Node while a call to patch is already executing.
Parameters
Node node
The Node to patch. Typically, this will be an HTMLElement or DocumentFragment.
function description
The description of the DOM tree underneath node.
any data
Optional data that will be passed to description.
Usage
import { patch } from 'incremental-dom';
function render(data) {
elementOpen('div');
elementClose('div');
…
}
var myElement = document.getElementById(…);
var someData = {…};
patch(myElement, render, someData);
Demos
Using Keys
The section on arrays of items mentions why using a key is important when iterating over an item. This demo shows how using a key prevents DOM nodes corresponding to separate items from being seen as a diff. In this case, a newly added item at the head of an array causes a new element by be created rather than all the items being updated.
Incremental DOM itself only renders Elements and Text nodes, but you may want to use components when building an application. One way this chould be solved is by using the emerging web components standards. The following demo shows one way you could create components.
Incremental DOM has two main strengths compared to virtual DOM based approaches:
The incremental nature allows for significantly reduced memory allocation during render passes, allowing for more predictable perfromance.
It easily maps to template based approaches. Control statements and loops can be mixed freely with element and attribute declarations.
Incremental DOM is a small (2.6kB min+gzip), standalone and unopinionated library. It renders DOM nodes and allows setting attributes/properties, but leaves the rest, including how to organize views, up to you. For example, an existing Backbone application could use Incremental DOM for rendering and updating DOM in place of a traditional template and manual update approach.