incremental-dom

Source

About

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.

Installation

See our Github.

Rendering DOM

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);

Attributes and Properties

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');

Statics Array

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');
  };
}

Applying Styles

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 a string

elementOpen('div', null, null,
    'style', 'color: white; background-color: red;');
      
elementClose('div');

As an object

elementOpen('div', null, null,
    'style', {
      color: 'white',
      backgroundColor: 'red'
    });
  
elementClose('div');

Conditional Rendering

If/else

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);
}

DOM Element Updates / Reuse

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’.

Keys

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:

  1. Prevent the treating of newly added or moved items as a diff that needs to be reconciled.
  2. Correctly maintain focus on any input fields, buttons or other items that may receive focus that have moved.

Keys do not need to be unqiue. One strategy might be to simple autogenerate a key for every logical statement in the source.

Keys and Arrays

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');

Logic in Attributes

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');

Rendering HTML Blobs

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.

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.

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;

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 Keys and Arrays
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.

Returns

Element The corresponding DOM Element.

Usage

import { elementOpen } from 'incremental-dom';

function somefunction() {  };



elementOpen('div', item.key, ['staticAttr', 'staticValue'],
    'someAttr', 'someValue',
    'someFunctionAttr', somefunction);

elementOpenStart

Description

Used with attr and elementOpenEnd to declare an element.

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 Keys and Arrays
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.

attr

Description

Used with elementOpenStart and elementOpenEnd to declare an element.

Parameters

string name
any value

elementOpenEnd

Description

Used with elementOpenStart and attr to declare an element.

Returns

Element The corresponding DOM Element.

elementClose

Description

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');

elementVoid

Description

A combination of elementOpen, followed by elementClose.

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.

Returns

Element The corresponding DOM Element.

Usage

import { elementClose } from 'incremental-dom';



elementVoid('div', item.key, ['staticAttr', 'staticValue'],
'someAttr', 'someValue',
'someFunctionAttr', somefunction);

text

Description

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');
  
}

const myElement = document.getElementById();
const someData = {};
patch(myElement, render, someData);

currentElement

Description

Provides a way to get the currently open element.

Returns

Element The currently open element.

Usage

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');

currentPointer

Description

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.

Returns

Node The next node that will be compared.

Usage

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);
}

skip

Description

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.

Usage

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);

skipNode

Description

Moves the current patch pointer forward by one node. This can be used to skip over elements declared outside of Incremental DOM.

Usage

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');
}

Demos

Using Keys

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.

Demo

Using with Web Components

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.

Demo

Animating When Reordering Items

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.

Demo

Why Incremental DOM

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 performance.
  • 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.

For more info read on here.