trackira

Virtual DOM boilerplate

Usage no npm install needed!

<script type="module">
  import trackira from 'https://cdn.skypack.dev/trackira';
</script>

README

#Trackira

Build Status devDependency Status npm version Coverage Status GitHub license

Virtual DOM libraries can be designed in many ways depending on the problems it is trying to solve, and it can be a challenging task to pick the right library to use.

Trackira try to solve this with being a boilerplate you can build upon. It's goal is not to be the fastest or the best, but possessing the best code structure and also be compatible with the DOM standards today.

Trackira output an Observable of "Virtual DOM Elements", to keep performance fast by patching the actual DOM with only the minimum necessary changes.

You can find a benchmark here

Supported features

  • Custom elements,
  • SVG and MathML support
  • xlink and xml namespace support
  • Type extensions
  • Server side rendering
  • mounting / unmounting
  • updates
  • patching
  • DOM level O events
  • lifeCycle hooks

Example

// creating the virtual tree
var tree = new Trackira.Tree();

// setting start count
var count = 0;

// creating a virtual node
function render(count) {
    return Trackira.h({tagName: "div",
        attrs: {
            style: {
                "text-align": 'center',
                "line-height": (100 + count) + 'px',
                border: '1px solid red',
                width: (100 + count) + 'px',
                height: (100 + count) + 'px'
            }
        }
    }, [count]);
}

var vnode = render(count);

// mount the virtual node
var mountId = tree.mount(document.body, vnode);

// render a new node and update the tree each second
setInterval(function () {
    count++;
    vnode = render(count);
    tree.update(mountId, vnode);
}, 1000);

View on JSFiddle

The above example can be compared with the example of virtual-dom.

Virtual nodes

Trackira.Element()

Trackira.Element() create a real DOM node, and accepts an optional data object and an optional string or array of children. E.g. new new Trackira.Element("div", {}, [new Text("Heeey!")]). Other virtual nodes are available as well - both text and comment node.

The optional data object contains this configurable values:

{
 key: String|Integer
 props {}
 attrs: {}
 events: {}
 data:()
 hooks: {}
}

Example:

var h = Trackira.h

var vnode = h({ tagName: "div", attrs: {style: {color: 'red'}}}, [
     h({tagName: "h1"}, ["Headline"]),
     h({tagName: "p"}, ["A paragraph"])
]);

View on JSFiddle

Attributes

Element attributes can be set with the attrs{} property. The attribute name are always the same as in HTML. If you provide a boolean for the attribute value. The attribute will be added with an empty string if it's a true value, and removed otherwise.

var h = Trackira.h;

// img element with src and alt attributes
h({tagName:"img",attrs: {src: 'http://...', alt: 'Image ...'}});

// input element with an id and class
h({tagName:"input", attrs: {id: "name-field", "class":"'important-field"});

// div element with a style object
h({tagName:"div", attrs: {style: 'border-bottom': '1px solid black', color: 'gray'}});

Non-ASCII symbols are supported and can be used example in class names.

h({tagName:"div", {attrs: {"class": "ΑΒΓΔΕΖ"}});

Inline style (css)

For inline style, you have to set the unit ( e.g. 'px') yourself. Values that need an unit, but don't have one, will not be set.

var h = Trackira.h;

// Element with style object
h({tagName:"div", attrs: {style: { height:"200px", width:"200px" }}});
// Element with style string
h({tagName:"div", attrs: {style: 'border-bottom': '1px solid black', color: 'gray'}});

Keys

Keys are unique and are attached to a virtual node, to ensure that only the elements which were really changed are updated. Even if elements were added/removed before or moved to another position. While common operations like appending and prepending nodes are optimized, it is also very fast at updating random changes like sorting or filtering.

// ensuring right focus
var mountId,
    h = Trackira.h,
    tree = new Trackira.Tree();

var from = [
    h({tagName: "input", key: 0, attrs: { id: "input_1", placeholder: "input_#1"}}),
    h({tagName: "input", key: 1, attrs: { id: "input_2", placeholder: "input_#2"}})
];

var to = [
    h({tagName: "input", key: 1, attrs: { id: "input_2", placeholder: "input_#2"}}),
    h({tagName: "input", key: 0, attrs: { id: "input_1", placeholder: "input_#1"}})
];
var active = false,
    updateFunc = function() {
        // negative value
        active = !active;
        return active ? from : to;
    },
    // mount
    mountId = tree.mount(document.body, updateFunc);

setInterval(function() {
    tree.update(mountId);
}, 1100);

View on JSFiddle

Children

 var h = Trackira.h;

// span element with one child
 h({tagName:"span"}, ["Hello World!]")

// span element with children
 h({tagName:"span"]}, ["Hello ", "World!"])

Virtual DOM node methods

Each virtual DOM node has it's own methods:

  • .render() - render the virtual node, and return a single DOM element
  • .toHTML() - create HTML markup server-side
  • .patch() - patch a virtual node with a real DOM node
  • .detach() - Remove a real DOM element from where it was inserted
  • .equalTo() - Checks if two virtual nodes are equal to each other, and they can be updated

Examples

node.render() // render the virtual node

node.patch(vnode) // patch the virtual node with a real DOM node

node.detach() // remove the node

node.equalTo(vnode) // returns boolean - true / false

Virtual tree

The API provides all necessary functions to create, update and remove virtual DOM nodes to/from the real DOM.

And many of the API features are the same as for other Virtual DOM libraries.

.mount()

Mounting to the document.body.

var tree = new Trackira.Tree(),
   foo = new Trackira.Element("h1", {}, ["Foo visited Bar"]);
tree.mount(document.body,  foo);

Mounting to element with an id - #mount-point.

tree.mount("#mount-point",  foo);

Mounting to element with a class - .mount-point.

tree.mount(".mount-point",  foo);

Mounting to element with DOM element - document.getElementById("test").

tree.mount(document.getElementById("test"),  foo);

You have probably realized that it supports CSS Selectors.

You can also mount with a factory - function.

var h = Trackira.h,
    tree = new Trackira.Tree(),
    render = (function() {

   var children = ["Hello, ", "World!!"];

   return h("div", children);
}())
// Mount the tree
 tree.mount("#mount-point", render);

With Trackira you also got more advanced options such as mounting with a unique ID identifer.

var bookedId = tree.guid();
var mountId = tree.mount("#mount-point1", h({tagName:"div"}, ["#1", "#2", "#3"]), {mountId: bookedId})

.unmount()

When you unmount a virtual tree, you can choose to unmount them all, or only one tree. Note that the unmount() function needs to be called with the mount identifier.

tree.unmount(mountID); // unmount a virtual tree with the mount identifier

tree.unmount(); // unmount all virtual trees

.update()

Once a virtual tree is mounted, you can update it. This API method takes one or two arguments. If no virtual node are set - as the second argument - it will only update already mounted node. E.g. changing / updating it's focus, or diff / patch the child nodes.

 tree.mount( uid); // update focus on already mounted node

With two arguments, you can update the existing node with another virtual node as shown in this example:

// create and mount a virtual node
var tree = new Trackira.Tree();
var h = Trackira.h;
var foo = h({tagName:"h1"}, ["Foo visited Bar"])
var mountId = tree.mount(document.body,  foo);

// new element to update
var newFoo = h({tagName:"h1"}, ["Bar was eating Foo"]);

// update the tree with the mount identifier
tree.update(mountId, newFoo);

View on JSFiddle

If want to update all mounted virtual trees, you can do it like this:

tree.update();

.mountPoint()

This method needs one argument - the unique number created when you mount a virtual tree

.children( uid ) // returns a real DOM node where the virtual tree are mounted

.children()

This method needs one argument - the unique number created when you mount a virtual tree

.children( uid ) // return a overview over all children to the mounted tree

.mounted()

.mounted() returns true if the virtual DOM node are mounted into the DOM, false otherwise. You can use this method to guard asynchronous calls.

.mounted( uid ) // return a boolean - true / false - if current tree are mounted
.mounted()      // returns a list over all mounted virtual trees

Detach

Trackira.detach() let you remove virtual nodes.

Example on detaching / removing an element node:

var container = document.createElement("div");
var node = new Trackira.Element("div");
var element = node.render();
container.appendChild(element);
// container.childNodes.length equal to 1

node.detach();
// container.childNodes.length equal to 0

View on JSFiddle

Patching operations

There are one main API methods for patching / diffing.

  • Trackira.patch()

For patching children of a real DOM node, you use Trackira.patch().

This method takes a DOM root node, and a set of DOM patches. The patching will only happen if the children are different.

Example patching a text node:

var h = Trackira.h;
var from = h({tagName:"div"}, ["hello", "to"]);
var to = h({tagName:"div"}, ["hello"]);
// patch the node
from.patch(to);

Another example:

// patching a HTML property
var h = Trackira.h;
var from = h({tagName:"div", props: { title: "hello" } });
var to = h({tagName:"div",  props: { title: "world" } });
// patch the node
from.patch(to);

Patch children on a real DOM node

var h = Trackira.h,
   oldChildren = h({tagName:"div"}, [h({tagName:"div"}]));
   newChildren = h({tagName:"div"});

// 'Trackira.patch' are only used for patching / diffing the children
Trackira.patch(node, oldChildren, newChildren);

Server rendring

Trackira supports server rendring. Use .toHTML() to turn virtual DOM nodes into HTML server-side. Properties get converted into attribute values.

// toHTML boolean properties
var element = Trackira.h({tagName:"input", props: {
    autofocus: true,
    disabled: false
    }
});
    html = element.toHTML();

// result:  <input autofocus>

View on JSFiddle

// convert properties to attributes
var element = Trackira.h({tagName:"form", props:{
    className: "login",
    acceptCharset: "ISO-8859-1",
    accessKey: "h"
    }
});
    html = element.toHTML();

// result:  <form class="login" accept-charset="ISO-8859-1" accesskey="h"></form>

View on JSFiddle

// .toHTML SVG attributes
var svg = Trackira.h({tagName:"circle", attrs: {
    cx: "60",
    cy: "60",
    r: "50"
    }
});
    html = svg.toHTML();

// result:  <circle cx="60" cy="60" r="50"></circle>

View on JSFiddle

// .toHTML a comment
var comment = Trackira.h("Foo like Bar");
    html = comment.toHTML();

// result:  <!-- Foo like Bar -->

View on JSFiddle

var node = Trackira.h({tagName:"div", props: { innerHTML: "<span>hello, terrible world!!</span>" } });
    html = node.toHTML();

// result: <div><span>hello, terrible world!!</span></div>"
// preserve UTF-8 entities and escape special html characters

var node = Trackira.h({tagName:"span"}, ["测试&\"\'<>"]);
    html = node.toHTML();

// result: <span>测试&amp;&quot;&#x27;&lt;&gt;</span

Event system

Trackira's is a cross-browser wrapper around the browser's native event, and each event are managed in a delegated way. The event handlers will be passed instances of SyntheticEvent, a cross-browser wrapper around the browser's native event. It has the same interface as the browser's native event, including stopPropagation() and preventDefault(), except the events work identically across all browsers.

To activate the event management, you wouldt need to initialize it with Trackira.initEvent() first. Like this:

Trackira.initEvent();

new Trackira.Element('div', {
    events: {
        onclick: function() {
            alert("Hello, world!");
        }
    }
});

After you have initialized the events, a set of common events are automatically bound to document body, and ready to be used.

Note!! You can use the events with or without the on prefix. E.g. onclick` or click.

Common event types

Trackira normalizes events so that they have consistent properties across different browsers and attach them to the current document. You controll this events with the bind() and unbind() API methods.

Supported common events:

  • blur
  • change
  • click
  • contextmenu
  • copy
  • cut
  • dblclick
  • drag
  • dragend
  • dragenter
  • dragexit
  • dragleave
  • dragover
  • dragstart
  • drop
  • focus,
  • input
  • keydown
  • keyup
  • keypress
  • mousedown
  • mousemove
  • mouseout
  • mouseover
  • mouseup
  • paste
  • scroll
  • submit
  • touchcancel
  • touchend
  • touchmove
  • touchstart
  • wheel

.bind() and .unbind()

This methods let you control the use of the common events. Here is an example:

// create virtual tree
var tree = new Trackira.Tree();

// initialize the events
var Evt = Trackira.initEvent();

// stop listen to the 'click' event
Evt.bind("click"); // try 'unbind()' to unbind event

// the event will not work because you unbinded it
var vnode = new Trackira.Element("button", {
    events: {
        onclick: function () {
            alert("Hello, world!");
        }
    }
}, [new Trackira.Text("Click me!")]);

// mount element
tree.mount(document.body, vnode);

View on JSFiddle

Show a set of events you are listening to.

// initialize the events
var Evt = Trackira.initEvent();

console.log(Evt.listeners());

Lifecycle Methods

Various methods are executed at specific points in a virtual node's lifecycle. This hooks are executed at different steps. Following hooks are provided:

  • updated - called every time an update occur on a virtual node
  • created - called once a virtual node has been created
  • detach - called when a virtual node is going to be removed
  • destroy - called on element removal

Animated transitions can be supported through "created" & "destroy" hooks.

// using 'created' hook on create
 var params,
 tag = new Trackira.Element("div", {
     hooks: {
        created: function() {  
           params = Array.prototype.slice.call(arguments);
        }
    }
});

 var element = tag.render();  // params are now equal to [tag, element]

Example on detach / destroy hooks when you are detaching a node

 var destroyHook;
 var h = Trackira.h;
 var params = [];
 var tag = h({tagName:"div", hooks: {
    detach : function() {
      params.push(Array.prototype.slice.call(arguments));
    },
    destroy : function(element, factory) {
      destroyHook = factory;
      params.push([element, destroyHook]);
    }
});

var mountId = tree.mount(document.body, tag);
tree.unmount(mountId);

// Console.log info:
//  mountPoint.innerHTML => <div></div>
//  params               => [[tag, tag.node], [tag.node, destroyHook]]

// Call the callback
destroyHook();

//  mountPoint.innerHTML => ""

Shadow DOM

Trackira supports Shadow DOM, and should work right out of the box with it. Shadow root is found and automatically dealt with.

Sources:

Many of the ideas for this virtual DOM are comming from the open source community. Many clever brains, with many clever ideas. Trackira is mainly inspired by

  • ReactiveJS ( text, comment and element prototype)
  • JSBlocks - ( virtual text and virtual comment nodes)
  • Virtual DOM ( the virtual DOM structure itself)
  • Kivi - Patching / diffing algorithm
  • citoJS - performance tips
  • REACT - mounting / unmounting and HTML markup server-side
  • vdom-to-html - for server side rendring
  • dom-layer - ideas about structure

Installing

Download the repo and run:

npm install

Commands:

Trackira uses gulp and Babel, and the commands are pretty straight forward:

// runs the unit tests
$ npm gulp

// build the library
$ npm gulp build

// bundle the library for our unit tests
$ npm gulp browserify

// show a istanbul coverage statistic on cmd line
$ npm gulp coverage

// runs unit tests
$ npm gulp test

// run the headless unit tests as you make changes
$ npm gulp watch

// set up a livereload environment for our spec runner
$ npm gulp test-browser

Community

  • Ask "how do I...?" questions on Slack:
  • Propose and discuss significant changes as a GitHub issues

Testing

All unit tests in the Travis CI are server-side. However. If you run this:

HTML file in your browser after you have cloned the repo, will you see unit tests running client-side.

Contribution

Want to contribute? Just send a pull request. All major development are in the dev branch. Master contains more or less a stable release.

LICENSE

The MIT License (MIT)