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 HTMLElement
s 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.
See our Github.
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.
patch(document.getElementById('someId'), renderPart);
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.
elementOpen('div', null, null,
'class', 'someClass',
'onclick', someFunction);
…
elementClose('div');
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.
function render() {
const s1 = [ 'type', 'text', 'placeholder', '…'];
return function(isDisabled) {
elementOpen('input', '1', s1,
'disabled', isDisabled);
elementClose('input');
};
}
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.
elementOpen('div', null, null,
'style', 'color: white; background-color: red;');
…
elementClose('div');
elementOpen('div', null, null,
'style', {
color: 'white',
backgroundColor: 'red'
});
…
elementClose('div');
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!
function renderGreeting(date, name) {
if (date.getHours() < 12) {
elementOpen('strong');
text('Good morning, ');
elementClose('strong');
} else {
text('Hello ');
}
text(name);
}
Incremental DOM compares the existing Element in the DOM with the one that is specified by the current function call. If the tag matches, it is reused. For example, given:
if (condition) {
elementOpen('div');
// subtree 'A'
elementClose('div');
}
elementOpen('div');
// subtree 'B'
elementClose('div');
If condition
goes from false
to true
, Incremental DOM will transform subtree
‘B’ into subree ‘A’. Next, it will create a new div
with subtree ‘B’.
To prevent different logical subtrees being reused, you can give Incremental DOM an additional signal to help distinguish between the two.
This is done by passing “key” to the elementOpen
function. This will be used
to:
Keys do not need to be unique. One strategy might be to simply autogenerate a key for every logical statement in the source.
Arrays can be especially important when rendering arrays, especially if you plan on adding new items in the start/middle or moving items. 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.
elementOpen('ul');
items.forEach(function(item) {
elementOpen('li', item.id);
text(item.text);
elementClose('li');
});
elementClose('ul');
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 (const key in obj) {
attr(key, obj[key]);
}
elementOpenEnd('div');
Incremental DOM does not have a way of rendering blobs of HTML itself, but it can be implemented using the skip
function. Depending on the complexity of what you need, a simple function like the following might suffice:
function html(content) {
const el = elementOpen('html-blob');
if (el.__cachedInnerHtml !== content) {
el.__cachedInnerHtml = content;
el.innerHTML = content;
}
skip()
elementClose('html-blob');
}
which could then be used as follows:
elementOpen('div');
text('Hello ');
html('<em>world</em>');
elementClose('div');
You may want something more advanced for your html rendering function. For example, you may want to make sure that the content is safe to render if it originates from a user (e.g. if it contains an anchor tag, the href
does not start with javascript:
) or that it is only called with strings that have already be sanitized (either on the backend or frontend).
Additionally, you may want to diff the HTML content rather than simply overwriting the existing content with innerHTML
.
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');
}
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.
import {
attributes,
applyProp,
applyAttr
} from 'incremental-dom';
attributes.value = applyProp;
If you would like to have a bit more control over how the value is set, you can specify your own function for applying the update.
attributes.value = function(element, name, value) {
…
};
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;
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
});
};
Declares an Element with zero or more attributes/properties that should be present at the current location in the document tree.
string tagname
string key
Array staticPropertyValuePairs
vargs propertyValuePairs
Element
The corresponding DOM Element.
import { elementOpen } from 'incremental-dom';
function somefunction() { … };
…
elementOpen('div', item.key, ['staticAttr', 'staticValue'],
'someAttr', 'someValue',
'someFunctionAttr', somefunction);
Used with attr
and elementOpenEnd
to declare an element.
string tagname
string key
Array staticPropertyValuePairs
Used with elementOpenStart
and elementOpenEnd
to declare an element.
string name
any value
Used with elementOpenStart
and attr
to declare an element.
Element
The corresponding DOM Element.
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
.
string tagname
Element
The corresponding DOM Element.
import { elementClose } from 'incremental-dom';
…
elementClose('div');
A combination of elementOpen
, followed by elementClose
.
string tagname
string key
Array staticPropertyValuePairs
vargs propertyValuePairs
Element
The corresponding DOM Element.
import { elementVoid } from 'incremental-dom';
…
elementVoid('div', item.key, ['staticAttr', 'staticValue'],
'someAttr', 'someValue',
'someFunctionAttr', somefunction);
Declares a Text
node, with the specified text, should appear at the current location in the document tree.
string|boolean|number value
...function formatters
Text
The corresponding DOM Text Node.
import { text } from 'incremental-dom';
function toUpperCase(str) {
return str.toUpperCase();
}
…
text('hello world', toUpperCase);
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.
Node node
function description
node
.any data
description
.import { patch } from 'incremental-dom';
function render(data) {
elementOpen('div');
elementClose('div');
…
}
const myElement = document.getElementById(…);
const someData = {…};
patch(myElement, render, someData);
Provides a way to get the currently open element.
Element
The currently open element.
import { elementOpen, elementClose, currentElement } from 'incremental-dom';
…
const element = elementOpen('div');
console.log(element === currentElement()); // true
elementOpen('span');
console.log(element === currentElement()); // false
elementClose('span');
console.log(element === currentElement()); // true
elementClose('div');
The current location in the DOM that Incremental DOM is looking at. This will be the next Node
that will be compared against for the next elementOpen
or text
call.
Node
The next node that will be compared.
This can be used to look at the next Node
to be diffed and perform some action (e.g. skip the node).
import { elementOpen, currentPointer, skipNode } from 'incremental-dom';
function myElementOpen(...args) {
// Skip over all nodes with _isExternal.
let pointer;
while (pointer = currentPointer() && pointer._isExternal) {
skipNode();
}
elementOpen(...args);
}
Moves the current pointer to the end of the currently open element. This prevents Incremental DOM from removing any children of the currently open element. When calling skip
, there should be no calls to elementOpen
(or similiar) prior to the elementClose
call for the currently open element.
import { patch, elementOpen, elementClose, skip } from 'incremental-dom';
function render(data) {
const element = elementOpen('div');
if (shouldUpdate(element._data, data)) {
element._data = data;
// Make calls to elementOpen, etc.
…
} else {
skip();
}
elementClose('div');
}
const myElement = document.getElementById(…);
const someData = {…};
patch(myElement, render, someData);
Moves the current patch pointer forward by one node. This can be used to skip over elements declared outside of Incremental DOM.
If you have a DOM structure like:
<div id="one"><!-- managed by Incremental DOM --></div>
<div id="two"><!-- maintained externally --></div>
<div id="three"><!-- managed by Incremental DOM --></div>
You can skip over <div id="two"></div>
as follows:
import { elementVoid, skipNode } from 'incremental-dom';
function render() {
elementVoid('div', null, null, 'id', 'one');
skipNode();
elementVoid('div', null, null, 'id', 'three');
}
The section on keys and arrays 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 simply creates and moves DOM nodes. There are no hooks for telling when an item moves or having any input into the process. You can use MutationObserver
to tell when things move and do fancy things like animate when items move within a the list. Animating out deletions can be done using a two step proccess where you render the item (but mark it as deleted), then do a later render where the item is actually removed.
Incremental DOM has two main strengths compared to virtual DOM based approaches:
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.
For more info read on here.