node package manager
Easy sharing. Manage teams and permissions with one click. Create a free org »

marko-vdom

marko-vdom

This module provides an optimized virtual DOM implementation where each virtual DOM node is API compatible with real DOM nodes for the minimal subset that is required to support DOM diffing/patching using morphdom.

Build Status Coverage Status NPM

Overview

Each virtual DOM node supports the following properties and methods required by morphdom:

  • node.firstChild
  • node.nextSibling
  • node.nodeType
  • node.nodeName
  • node.namespaceURI
  • node.nodeValue
  • node.attributes [1]
  • node.value
  • node.selected
  • node.disabled
  • node.actualize(document) [2]
  • node.hasAttributeNS(namespaceURI, name)
  • node.isSameNode(anotherNode) [3]
  • node.assignAttributes(targetNode) [4]

NOTES:

  1. Unlike with real DOM nodes, node.attributes can either be an Array of Attr objects or an Object (where each property represents an attribute. e.g., { "class": "foo", "id": "bar" })
  2. In addition to the standard DOM node methods and properties, a virtual DOM node must also provide a node.actualize(document) method. The node.actualize(document) will be called when the virtual DOM node needs to be upgraded to a real DOM node so that it can be moved into the real DOM.
  3. A virtual DOM node may choose to implement isSameNode(anotherNode) to short-circuit diffing/patching a particular DOM subtree by treating two nodes as the "same"
  4. A virtual DOM node may choose to implement the non-standard assignAttributes(targetNode) to optimize copying the attributes from the virtual DOM node to the target DOM node

marko-vdom is namespace aware and will work correctly with SVG and MathML elements.

While marko-vdom exposes an API that can be used directly, the terse API is designed to be used with a compiler that generates JavaScript code.

Usage

Create an element with a fixed number of attributes and a fixed number of children

var createElement = require('marko-vdom').createElement;
 
createElement('div', { class: 'foo', onclick: 'doSomething()' }, 2 /* childCount */)
    .e('span', null, 1)
        .e('b', null, 1)
            .t('Hello World!')
    .e('a', { href: 'http://ebay.com' }, 1)
        .t('eBay')

The above code will generate a virtual DOM tree that mirrors the following:

<div class="foo" onclick="doSomething()">
    <span>
        <b>Hello World!</b>
    </span>
    <a href="http://ebay.com">eBay</a>
</div>

Dynamic HTML with a unknown number of children

var createElement = require('marko-vdom').createElement;
 
var el = createElement('div', { class: 'foo' });
el.appendChild(createElement('span', { class: 'bar' }));

The above code will generate a virtual DOM tree that mirrors the following:

<div class="foo">
    <span class="bar"></span>
</div>

Static subtree

var createElement = require('marko-vdom').createElement;
 
var staticLink = createElement('a', { href: 'http://ebay.com' }, 1 /* childCount */, 'abc123' /* key */)
    .t('eBay')
 
function render() {
    createElement('div', null, 1 /* childCount */)
        .n(staticLink);
}

The above code will generate a virtual DOM tree that, when converted to a real DOM, will be the following:

<div>
    <a href="http://ebay.com" data-marko-same-id="abc123">
        eBay
    </a>
</div>

For the static link, both the virtual DOM node and the real DOM node will be marked with an "id" that identifies the two nodes as the "same" node in order to short-circuit DOM/diffing patching. That is:

var realStaticLink = staticLink.actualize(document);
console.log(staticLink.isSameNode(realStaticLink)); //Output: true 

Document fragments

Document fragments are containers for child nodes that can be appended as children nodes, but the actual DocumentFragment node is never directly visited when walking the DOM using node.firstChild and node.nextSibling. Instead, the children (if any) of a DocumentFragment node are treated as direct children of the parent of the DocumentFragment node. A DocumentFragment node can be modified with new children even after it has been inserted into the DOM.

var createElement = require('marko-vdom').createElement;
 
var div = createElement('div');
documentFragment.appendChild(createElement('div'));
 
var documentFragment = div.appendDocumentFragment();
documentFragment.appendChild(createElement('span', { class: 'foo' }));
documentFragment.appendChild(createElement('span', { class: 'bar' }));
 
/*
 
Output DOM:
 
<div>
    <span class="foo"></span>
    <span class="bar"></span>
</div>
*/
 

Benchmarks

This library includes some benchmarks to compare performance with the real DOM (and React). To run the benchmarks:

npm run benchmark

This will open a web page in your browser that you can use to run a variety of benchmarks.

We are interested in the following performance characteristics:

  • Creation time - the time it takes to construct a [virtual] DOM tree
  • Walk time - the time it takes to walk a [virtual] DOM tree using firstChild and nextSibilng

We encourage you to run the benchmarks on your machine and in various browsers. If you see any problems with the benchmarks, or if you would like clarifying information please open a Github issue.

Please see Benchmark Results for more detailed numbers.

API

marko-vdom

Methods

createElement(tagName, attrCount, childCount, key)

Returns a new HTMLElement.

createText(value)

Returns a new Text.

createComment(value)

Returns a new Comment.

createAttributes(attrCount)

Returns a new AttributeCollection.

createDocumentFragment()

Returns a new DocumentFragment


AttributeCollection

Methods

a(name, value)

as(attributes)


Comment


DocumentFragment


HTMLElement

Constructors

HTMLElement(tagName, attrCount, childCount, key)

Parameters:

  • tagName - The tag name for the new HTML element (String)
  • attrCount - The number of attributes (if known) (an integer, null or undefined)
  • childCount - The number of child nodes (if known) (an integer, null or undefined)
  • key - A key for static nodes to use for isSameNode() checks

HTMLElement(htmlElement)

Used to do a shallow clone of another HTMLElement

Properties

nodeType

Always set to 1

Methods

a(name, value) : Node

See AttributeCollection#a

actualize(document) : HTMLElement

Converts the virtual HTMLElement tree to a real HTMLElement tree using the provided document.

as(name, value) : Node

See AttributeCollection#a

appendDocumentFragment() : DocumentFragment

See Node#appendDocumentFragment

c(value) : Node

Shorthand method for creating a Comment node and appending it as a child.

cloneNode() : HTMLElement

Performs a shallow clone of the node (nextSibling and parentNode will be undefined since a cloned node will always start out as detached)

e(tagName, attrCount, childCount, key) : Node

Shorthand method for creating an HTMLElement node and appending it as a child.

hasAttributeNS(namespaceURI, name) : boolean

isSameNode(otherNode) : boolean

Called by morphdom to determine if the target HTMLElement (either virtual or real) node is the same as the current node. The key passed in to the constructor is used to do determine if the other node is the "same" node. If the other node is a real DOM node then the key is pulled from the data-markokey attribute.

n(node) : Node

Shorthand method for appending a node as a child. The provided is automatically cloned (using a shallow clone) since it is assumed that this method will be called to append a static/memoized DOM node and the original DOM node should not be modified.

t(value) : Node

Shorthand method for creating a Text node and appending it as a child.


Node

Properties

firstChild : Node

Returns the first child node

nextSibling : Node

Returns the next sibling node

Methods

appendDocumentFragment() : DocumentFragment

Creates and appends a new DocumentFragment node and appends it as a child and the newly created DocumentFragment node is returned.

removeChildren()

Clears out all child nodes


Text