Skip to content
Mikado is the webs fastest and most lightweight templating engine.
JavaScript HTML
Branch: master
Clone or download
Latest commit 22944a3 Oct 17, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bench ADD fixes and tests Oct 17, 2019
demo FIX basic examples Oct 16, 2019
dist ADD fixes and tests Oct 17, 2019
doc ADD reconciler concepts Oct 9, 2019
src ADD fixes and tests Oct 17, 2019
task ADD keyed paradigm + cross-shared pools Oct 5, 2019
test ADD fixes and tests Oct 17, 2019
.gitignore MOD make todomvc demo available Sep 18, 2019
.travis.yml ADD automated test Sep 20, 2019
CHANGELOG.md ADD minor fixes + new options "keep" and "size" Oct 9, 2019
LICENSE -- INITIAL COMMIT -- Sep 6, 2019
README.md ADD fixes and tests Oct 17, 2019
package.json ADD fixes and tests Oct 17, 2019

README.md

Mikado - Webs fastest templating engine

Web's fastest templating engine. Super-lightweight, outstanding performance, no dependencies.

Rendering has by far the most impact on application performance. Mikado takes templating performance to a whole new level and provides you keyed and non-keyed paradigm switchable out of the box. Let's start building the next generation of high performance applications.

Getting Started  •  Options  •  API  •  Concept  •  Custom Builds  •  Template Compiler  •  Template Server  •  Express Middleware (SSR)  •  Changelog

Services:

Mikado Runtime (Render Templates)
npm install mikado

Mikado Compiler (Compile Templates)
npm install mikado-compile

Mikado Server (Serve Templates)
npm install mikado-server

Express Middleware (Server-Side Rendering) *WIP*
npm install mikado-express

Benchmark:

Demo:

  1. Basic Example (Classic Javascript)
  2. Basic Example (ES6 Modules)
  3. Runtime Compiler
  4. TodoMVC App: Source Code / Run Demo
  5. js-framework-benchmark

Comming Soon

new webpack loader to bundle templates
change file endings for templates are customizable (e.g use .shtml)

Table of contents

  1. Get Latest
  2. Feature Comparison: Mikado Light
  3. Benchmark Ranking (Rendering Performance)
  4. API Overview
  5. Options
  6. Template Compiler
  7. Rules and Conventions
  8. Basic Example
  9. Advanced Example
  10. Event Bindings
  11. Keyed / Non-Keyed Modes
  12. Non-Reusing
  13. Usage:
  14. DOM State Caching
  15. Storage
  16. State
  17. Transport / Load Templates
  18. Static Templates
  19. Compiler Service / Live Templates
  20. Template Features:
  21. Reactive Proxy (Observer)
  22. Best Practices
  23. Memory Optimizations
  24. Concept of Shared Pools
  25. Custom Builds

Get Latest:

Build File CDN
mikado.min.js Download https://rawcdn.githack.com/nextapps-de/mikado/master/dist/mikado.min.js
mikado.light.js Download https://rawcdn.githack.com/nextapps-de/mikado/master/dist/mikado.light.js
mikado.custom.js Custom Build

Recommended: To get a specific version just replace /master/ with one of the version numbers from the release e.g. /0.3.1/, or also a commit hash.

Node.js

Install Mikado via NPM:

npm install mikado

You can also classically include one of the distributed builds as a script tag or use the sources as ES6 modules.

To make the command line interface available you have to install via NPM.

Feature Comparison

Feature mikado.min.js mikado.light.js
Template Engine
DOM State Caching
Shared Pools / Live Pools
Keyed/Non-Keyed
Strict Non-Reusing
Runtime Compiler -
Manage Data Store -
Event Binding/Routes -
Data Proxy (Observe) -
Asynchronous Render -
Transport/Load Templates -
Export/Import Views -
DOM Manipulation Helpers -
Conditional Branches -
Includes/Partials/Loops -
File Size (gzip) 6.8 kb 2.8 kb

Benchmark Ranking (Rendering Performance)

Run the benchmark (non-keyed):
https://raw.githack.com/nextapps-de/mikado/master/bench/

Run the benchmark (keyed):
https://raw.githack.com/nextapps-de/mikado/master/bench/?keyed

Run the benchmark (internal/data-driven):
https://raw.githack.com/nextapps-de/mikado/master/bench/?internal

Sources and readme:
https://github.com/nextapps-de/mikado/tree/master/bench

Values represents operations per second, each benchmark task has to process a data array of 100 items. Higher values are better, except file size (minified/gzip) and memory (sum of allocation during the whole test).

Library KB RAM Create Replace Update Order Repaint Append Remove Toggle Clear Index Score
mikado 2.8 38 18850 7611 38162 27155 248338 32852 26501 33436 26448 999 21597
lit-html 17.3 370 437 3782 19920 5954 42887 904 8159 1623 4729 247 3572
inferno 8.4 325 728 667 4781 4777 5743 1297 5929 2187 12255 196 1601
mithril 9.6 242 627 2032 4048 2622 4738 1101 6448 1921 10784 199 1501
redom 2.9 337 431 2342 4156 2774 4449 793 6145 1455 11965 239 1496
domc 4.5 393 1154 1101 1115 1096 1087 1129 2005 1460 26123 228 1103
ractive 68.2 1405 165 1359 5099 1940 7836 334 2869 605 1693 90 977
innerhtml 0 545 1003 970 857 844 849 880 1582 1128 26836 162 916
surplus 15.8 563 906 779 782 791 787 824 1530 1074 23952 172 832
sinuous 7.5 662 833 796 759 778 775 787 1422 1043 16578 162 783
jquery 31.3 684 784 693 628 628 626 675 1091 831 5370 90 573
knockout 24.8 2676 86 64 65 65 65 80 122 97 1115 48 147

The file size and memory gets less relevance. The maximum possible index is 1000, that requires a library to be best in each category. The score value is relational where a score of 1000 represents the statistical midfield.

Read more about this test here.

API Overview

Most of these methods are optionally, you can just use .render() to apply all changes automatically.

Constructor:

Global methods:

Global methods (not included in mikado.light.js):

Instance methods:

Instance methods (not included in mikado.light.js):

DOM manipulation helpers (optional, not included in mikado.light.js):

Instance properties:

Global helpers (optional, not included in mikado.light.js):

Options

Each Mikado instance can have its own options.

Option Description Default
root The destination root where the template should be rendered. null
template The template which should be assigned to the Mikado instance (JSON or the name of the template when registered/loaded externally).
async Perform render tasks asynchronously and return a Promise. false
cache Enable/disable caching. Caching can greatly increase performance (up to 20x). false
store Passed data for rendering are also stored and synchronized along the virtual dom. You can re-render the full state at any time, without passing the data.
Notice: When passing an external reference of an existing Array-like object to the field "store" the store will perform all modifications directly to this reference (read more about "Extern Storage").
false
loose When storage is enabled this flag removes also data whenever a corresponding dom element was removed. false
reuse When enabled all dom elements which are already rendered will be re-used for the next render task. This performs better, but it may produce issues when manual dom manipulations was made which are not fully covered by the template. Whe enabled make sure to use the Virtual DOM Manipulation helpers. true
state Pass an extern object which should be referenced as the state used within template expressions. { }
pool When enabled, a shared pool will be used. Requires reuse also to be enabled. true
size Sets the maximum size of the shared pool. When not set or false it uses "auto scaling". false
keep When enabled, Mikado uses an exclusive shared keyed pool. Requires pool to be false. false
prefetch Prefetch/prebuilt a template on page load. Disable to save memory an speed up page start. true

Compiler Methods

Method Memory Performance Notes
Mikado Compiler (CLI) best 6,156,704 op/s
  • good for production
  • bundle templates easily out of the box
Compiler Service (Server) best (server-side)
  • good for production
  • bundle not required
  • best caching/re-using capabilities
  • live updates
HTML5 Templates (Runtime) good 53,715 op/s
  • good for development
  • bundle templates requires additional tooling (like webpack)
Template String (Runtime) medium 15,062 op/s
  • good for development
  • bundle templates easily out of the box

Note: These benchmark results are just representing the duration of compiling. Choosing a specific compiler method has no impact on the render performance.

1. Variant: Using Dedicated Compiler (Recommended)

Define a HTML-like template and use double curly brackets to mark dynamic expressions which should be calculated during runtime:

<table>
    <tr>
        <td>User:</td>
        <td>{{data.user}}</td>
    </tr>
    <tr>
        <td>Tweets:</td>
        <td>{{data.tweets.length}}</td>
    </tr>
</table>

Save this template e.g. to user/list.html

The preserved keyword data is a reference to a passed data item. You can access the whole nested object.

Mikado comes up with a template compiler which has to be run through Node.js and provides a command line interface (CLI) to start compilation tasks. The template compiles into a fully compatible JSON format and could also be used for server-side rendering.

Install Mikado Compiler via NPM:

npm install mikado-compile

Compile the template through the command line by:

npx mikado compile user/list.html

Notation: npx mikado compile {{input}} {{destination}}

Instead of npx mikado compile you can also use npx mikado-compile alternatively. When a destination was not set, the input folder will be used instead.

After compilation you will have 4 different files:

  1. template.js the template compiled in ES5 compatible Javascript
  2. template.es6.js the template compiled as an ES6 module
  3. template.json the template compiled in JSON-compatible notation (to load via http request)
  4. template.html the HTML-like template (reference, do not delete it)

2. Variant: Using HTML5 Templates

Define in HTML:

<template id="user-list">
    <table>
        <tr>
            <td>User:</td>
            <td>{{data.user}}</td>
        </tr>
        <tr>
            <td>Tweets:</td>
            <td>{{data.tweets.length}}</td>
        </tr>
    </table>
</template>

Use runtime compiler:

var tpl = Mikado.compile("user-list");
var tpl = Mikado.compile(document.getElementById("user-list"));

Create mikado view:

var view = Mikado.new(tpl);

3. Variant: Using Template String

Define HTML as string:

const template = ( 
    `<table>
        <tr>
            <td>User:</td>
            <td>{{data.user}}</td>
        </tr>
        <tr>
            <td>Tweets:</td>
            <td>{{data.tweets.length}}</td>
        </tr>
    </table>`
);

Use runtime compiler:

var tpl = Mikado.compile(template);

Create mikado view:

var view = Mikado.new(tpl);

Basic Example

Assume there is an array of data items to render (or just one item as an object):

var data = [{
    user: "User A",
    tweets: ["foo", "bar", "foobar"]
},{
    user: "User B",
    tweets: ["foo", "bar", "foobar"]
},{
    user: "User C",
    tweets: ["foo", "bar", "foobar"]
}]

Load library and initialize template (ES5):

<script src="mikado.min.js"></script>
<script src="user/list.js"></script>
<script>
    var view = Mikado.new("template");
</script>

The name of a template inherits from its corresponding filename.

Load library and initialize template (ES6):

<script type="module">
    import Mikado from "./mikado.js";
    import template from "./user/list.es6.js";
    var view = Mikado.new(template);
</script>

After creation you need mount to a DOM element initially as a destination root and render the template with populated data:

view.mount(document.body);
view.render(data);

You can also chain methods:

var view = Mikado.new(template).mount(document.body).render(data);

Rules and Conventions

Every template has to provide one single root as the outer bound. This is a convention based on the concept of Mikado.

Instead of doing this in a template:

<header>
    <nav></nav>
</header>
<section>
    <p></p>
</section>
<footer>
    <nav></nav>
</footer>

Provide one single root by doing this:

<main>
    <header>
        <nav></nav>
    </header>
    <section>
        <p></p>
    </section>
    <footer>
        <nav></nav>
    </footer>
</main>

You can also use a <div> or any other element as a template root (also custom elements).

Mixing text nodes and child nodes within same root is actually not possible:

<main>
    {{ data.title }}
    <section>{{ data.content }}</section>
    {{ data.footer }}
</main>

This may provided in the future, in the meanwhile just wrap text nodes into its own child:

<main>
    <title>{{ data.title }}</title>
    <section>{{ data.content }}</section>
    <footer>{{ data.footer }}</footer>
</main>

This example has not this issue, because text nodes and child nodes are not mixed:

<main>
    <section>{{ data.title }} foobar {{ data.footer }}</section>
</main>

Advanced Example

A bit more complex template:

<section id="{{data.id}}" class="{{this.state.theme}}" data-index="{{index}}">
    {{@var is_today = data.date === view.today}}
    <div class="{{data.class}} {{is_today ? 'on' : 'off'}}">
        <div class="title" style="font-size: 2em">{{data.title.toUpperCase()}}</div>
        <div class="content {{index % 2 ? 'odd' : 'even'}}">{{#data.content}}</div>
        <div class="footer">{{view.parseFooter(data)}}</div>
    </div>
</section>

You can use any Javascript within the {{ ... }} curly bracket notation.

To pass html markup as a string, the curly brackets needs to be followed by # e.g. {{# ... }}. For performance relevant tasks avoid passing html contents as string.

To use Javascript outside an elements context you need to prevent concatenation of the returned value. For this purpose the curly brackets needs to be followed by @ e.g. {{@ ... }}.

Within a template you have access to the following indentifiers:

Identifier Description
data A full reference to a passed data item.
view An optional payload used to manually pass in non-data-item specific values or helper functions.
index Represents the index of the currently rendered data item.
self Points to the current rendered element itself. Using "js" node property or by using the {{@ marker grants you to have "self" available.
this Provides you access to the Mikado view instance.
this.state An object used to keep data as a state across runtime. You can share state data across all Mikado instances by passing the same external object reference during initialization.
this.store Gives access to the internal data store (Array).
window The global namespace.

You cannot change the naming of those preserved keywords.

It is recommended to pass custom functions via the view object (see example above). You can also nest more complex computations inline as an IIFE and return the result.

<div class="date">{{ 
    (function(){ 
        var date = new Date();
        // ...
        return date.toLocaleString(); 
    }()) 
}}</div>

Alternatively of accessing data, view, index and this.state you can also access variables from the global namespace.

To finish the example above provide one single object or an array of data item:

var data = [{
    id: "230BA161-675A-2288-3B15-C343DB3A1DFC",
    date: "2019-01-11",
    class: "yellow, green",
    title: "Sed congue, egestas lacinia.",
    content: "<p>Vivamus non lorem <b>vitae</b> odio sagittis amet ante.</p>",
    footer: "Pellentesque tincidunt tempus vehicula."
}]

Provide view payload (non-data-item specific values and helper methods used by the template):

var payload = {
    page: 1,
    today: "2019-01-11",
    parseFooter: function(data){ return data.footer; }
}

Provide state data (application specific data and helper methods used by the template):

view.state.theme = "custom";

Create a new view instance or initialize a new template factory to an existing instance:

view.init(template);

Mount to a new target or just render the already mounted template:

view.render(data, payload);

Render asynchronously by providing a callback function:

view.render(data, payload, function(){
    console.log("finished.");
});

To render asynchronously by using promises you need to create the view instance with the async option flag:

view = Mikado.new(template, { async: true });

view.render(data, payload).then(function(){
    console.log("finished.");
});

Event Bindings

Lets take this example:

<table data-id="{{data.id}}" root>
    <tr>
        <td>User:</td>
        <td click="show-user">{{data.user}}</td>
        <td><a click="delete-user:root">Delete</a></td>
    </tr>
</table>

There are 2 click listeners. The attribute value represents the name of the route. The second listener has a route separated by ":", this will delegate the event from the route "delete-user" to the closest element which contains the attribute "root".

Define routes:

view.route("show-user", function(node, event){

    alert(node.textContent);

}).route("delete-user", function(node, event, self){

    alert(node.dataset.id); // delegated to "root"
    console.log("The element who fires the event: ", self);
})

Routes are stored globally, so you can share routes through all Mikado instances.

List of all supported events:

  • tap (synthetic touch-enabled "click" listener, see below)
  • change, input, select, toggle
  • click, dblclick
  • keydown, keyup, keypress
  • mousedown, mouseenter, mouseleave, mousemove, mouseout, mouseover, mouseup, mousewheel
  • touchstart, touchmove, touchend
  • submit, reset
  • focus, blur
  • load, error
  • resize
  • scroll

Synthetic events:

Event Description
tap The tap event is a synthetic click event for touch-enabled devices. It also fully prevents the 300ms click delay. The tap event automatically falls back to a native click listener when running on non-touchable device.

Explicit Register/Unregister

You can also use the event delegation along with "routes" outside a template. Just apply the event attribute like you would do in a template.

<body>
    <div click="handler">Click Me</div>
</body>
Mikado.route("handler", function(target, event){
    console.log("Clicked");
});

Then you have to explicit register these event once:

Mikado.listen("click");

Because the register of events basically happens when creating the template factory under the hood. When no template was created which includes the same type of event, then there exist is no global listener. For that reason you have to explicitly register the listener once.

Same way you could also unregister events:

Mikado.unlisten("click");

Keyed/Non-Keyed Modes

Each template instance can run in its own mode independently.

Compare benchmark of all supported modes here:
https://raw.githack.com/nextapps-de/mikado/master/bench/?modes

1. Non-Keyed

A non-keyed strategy will reuse all existing components and is basically faster than keyed but also has some side-effects when not used properly.

Just provide a template as normal:

<div>
    <div>User:</div>
    <div>{{data.name}}</div>
</div>

along with these options:

var view = Mikado.new(template, { pool: true });

This will switch Mikado into a "non-keyed" mode where already rendered components will be re-used. Using the pool is optional.

2. Explicit Keyed (Non-Shared)

A keyed strategy limits re-usability of components based on items with same ID. It just requires an unique identifier on each rendered item (e.g. the ID).

Add the attribute key to the root element of a template (or the root of an inline partial) and assign the namespace to the unique identifier:

<div key="data.id">
    <div>User:</div>
    <div>{{data.name}}</div>
</div>

To make them explicitly keyed also disable reusing:

var view = Mikado.new(template, { reuse: false });

This will switch Mikado into a "explicit keyed" mode (non-shared).

3. Explicit Keyed (Shared)

This is a special mode which uses the shared keyed index exclusively (without pooling). This will give you the absolute maximum performance, but it has a limit you should keep in mind when using this mode. The exclusive keyed mode is unbounded. Just use this mode on templates where the amount of incoming data is supposed to be limited (e.g. in a common scenario: pagination through a set of x items, like a todo list). Otherwise you will get no performance gain and also the memory allocation increases constantly (unbounded).

<div key="data.id">
    <div>User:</div>
    <div>{{data.name}}</div>
</div>

along with these options:

var view = Mikado.new(template, { reuse: false, keep: true });

This will switch Mikado into a "explicit keyed" mode (shared).

4. Cross-Shared (Hybrid)

The cross shared mode is a hybrid and takes the performance benefits of both shared pools and provides you an enhanced pooling of reusable components. This mode provide high performance as well as low memory allocation during runtime.

Add the attribute key to the root element of a template:

<div key="data.id">
    <div>User:</div>
    <div>{{data.name}}</div>
</div>

along with these options:

var view = Mikado.new(template, { pool: true } );

This will switch Mikado into a "cross-shared-keyed" mode.

5. Exclusive-Shared (Hybrid)

You can also use the same strategy from 3. for hybrid mode. But it has the same limits as 3., read above.

<div key="data.id">
    <div>User:</div>
    <div>{{data.name}}</div>
</div>

along with these options:

var view = Mikado.new(template, { pool: false, keep: true });

This will switch Mikado into a "exclusive-shared-keyed" mode.

Non-/Reusing

Mikado is one of the very few libraries which provides you a 100% non-reusing paradigm out of the box.

Generally keyed libraries will fail in restoring the original state of a component when a data item of the new fetched list has the same key. As long you follow some restrictions this may not an issue. But whenever you get in situations where you have to force restoring, every keyed lib will fail and you may have to use quick fixes like randomize the ID of the component. Also keyed libs cannot fully integrated into every stack, especially when additional UI libs where used.

Mikado is able to restoring 100% of the original state. This helps in situations where:

  • external libraries changes components nodes
  • event listeners was bind directly to components nodes
  • external data/values was referenced to components nodes
  • components nodes where manually manipulated
  • the process workflow requires redrawing of the original state on new data (required by some MVC)
  • you need integration in a stack without side effects

Notice: An original state does not include an event listener which was directly bound to an element. The original state is the state before you apply anything manually (or by external).

Render vs. Refresh

When reusing was disabled you can take advantage of Mikados 2 different render functions.

Whenever you call .render() when reusing was disabled all components will be recreated (restoring original state):

view.render(items);

Recreation has a significant cost and is often not strongly required by every single render loop. When using a store you can made changes to the data and just commit the changes when finished:

view.store[1]["title"] = "new title";
view.refresh(1);

The refresh method will just apply data changes to the view without restoring the original state by recreation of its components.

You can also refresh all components lazily when doing multiple changes:

view.store[1].title = "new title";
view.store[2].content = "new content";
view.store[3].footer = "new footer";
view.refresh();

It is pretty much the same when using stores in loose mode:

view.data(1).title = "new title";
view.data(2).content = "new content";
view.data(3).footer = "new footer";
view.refresh();

Passing a components root node or an index to the refresh method performs faster than passing no parameter.

Hint: You can also use the refresh method when new items was pushed (added to the end) or was removed from the end. You cannot use refresh when new items was inserted/removed/arranged/replaced before the end, this requires .render().

Create, Initialize, Destroy Views

Create view from a template with options:

var view = Mikado.new(template, options);

Create view from a template with options and also mount it to a target element:

var view = Mikado.new(root, template, options);

Mount a view to a target element:

view.mount(element);

Initialize an existing view with new options:

view.init(options);

Initialize an existing view also with a new template:

view.init(template, options);

Unload/delete the template which is assigned to a view:

view.unload();

Destroy a view:

view.destroy();

Render Templates

When using an internal storage (not external), every render task also updates the storage data.

Render a template incrementally through a set of data items:

view.render(data);

Render a template via data and also use view-specific data/handlers:

view.render(data, payload);

Schedule a render task asynchronously to the next animation frame:

view.render(data, payload, true);

Schedule a render task by using a callback:

view.render(data, payload, function(){
    // finished
});

Schedule a render task by using promises (requires option async to be enabled during initialization):

view.render(data, payload).then(function(){
    // finished
});

Render a static template (which uses no dynamic content):

view.render();

Repaint the current state of a dynamic template (which has data, requires store to be enabled):

view.refresh();

Repaint the current state on a specific index:

view.refresh(index);

Just create an template without adding/assigning/rendering them to the root ("orphan"):

var partial = view.create(data);

Orphans are not a part of the internal render tree. The construction of orphans is really fast. You could also use the light version of Mikado an apply your own stack on top of this method.

Modify Views

Add one data item to the end:

view.add(data);

Add one data item to a specific index (did not replace):

view.add(data, 0); // add to beginning

Append multiple data items to the end:

view.append(data);

Append multiple data before an index:

view.append(data, 0); // append to beginning

Remove a specific data item/node:

view.remove(node);

Remove a specific template node by its index:

view.remove(20);

Remove a range of nodes starting from a specific node/index:

view.remove(20, 10);
view.remove(node, 20);

Remove last 20 node items (supports reverse index):

view.remove(-20, 20);

Remove previous 20 node items starting of a given node/index (including):

view.remove(node, -20);

Remove all:

view.clear();

Replace a data item/node:

view.replace(old, new);

Update an single data item/node:

view.update(node, data);

Re-Sync DOM:

view.sync();

Re-Sync DOM + Release Cache:

view.sync(true);

Purge all shared pools (factory pool and template pool):

view.purge();

Useful Helpers

Get a template root node from the DOM by index:

var node = view.node(index);

Get a data item from the store by index:

var data = view.data(index);

Get a data item from the store by node:

var data = view.data(node);

Get the index from a given node:

var index = view.index(node);

Find a node which corresponds to a data item (same reference):

var node = view.find(data);

Find the first node which corresponds to a data item which has same content (that may requires each data item to be unique, otherwise use where):

var node = view.search(data);

Find all nodes which matches a given payload (will always return an array, empty if no results was found):

var node = view.where({
    title: "foo",
    active: true,
    content: "bar"
});
var node = view.where(data);

Get the length of all data items rendered (in store):

var length = view.length;

Get the current template name which is assigned to a Mikado instance:

var name = view.template;

Manipulate Views

Manual changes on the DOM may require re-syncing. To prevent re-syncing Mikado provides you several helper functions to manipulate the dom and also keep them in sync. Using the helper function also grants you a maximum performance.

Move a data item/node to a specific index:

view.move(node, 15);  // 15 from start
view.move(node, -15); // 15 from end

Move a data item/node to the top or bottom:

view.first(node);
view.last(node);

Move a data item/node by 1 index:

view.up(node);
view.down(node);

Move a data item/node by a specific offset (pretty much the same as shift):

view.up(node, 3);
view.down(node, 3);

Shift a data item/node relatively by a specific offset (both directions):

view.shift(node, 3);
view.shift(node, -3);

Move a data item/node before or after another data item/node:

view.before(node_a, node_b);
view.after(node_a, node_b);

Swap two data items/nodes:

view.swap(node_a, node_b);

DOM State Caching

Caching of DOM properties can greatly increase performance (up to 20x). There are just few situations where caching will not improve performance, it fully depends on your application.

Recommendation: enable caching when some of your data will stay unchanged from one to another render task. Disable caching when changes on data requires a fully re-render more often.

Caching is by default enabled, this may change in future, so best is to explicitly set this flag when initializing:

var view = new Mikado(template, { cache: true });

We strongly recommended to read the next section to understand how caching is working.

The Concept

Let's take a simple template as an example:

<root>
    <div class="active">{{ data.title }}</div>
</root>

The template above has just one dynamically expression. It could be rendered as follows:

view.render({ title: "foobar" });

Assume you get new data and wants to update the view, but the new data has still the same value for title:

view.render({ title: "foobar" });

This time, the property will not changed. That specific part now executes more than 10,000 times faster. Make maximum use of this strategy will speed up things amazingly.

When caching is enabled it automatically applies for all dynamic expressions within a template by default.

So whenever you like to change one of the nodes attributes or contents (e.g. style, class, properties, dataset, etc) you just wrap this as an expression within the template and it will apply automatically.

For example, when you would like to change the classname also, then just wrap in as an expression:

<root>
    <div class="{{ view.active }}">{{ data.title }}</div>
</root>

You do not have to use data only, you can also use a payload view or the state property. Using them right increases the flexibility of template re-using.

Now lets come to the most important part when using caching properly. Assume you have rendered the template above with caching enabled. Now you manually change DOM attributes:

var node = document.getElementsByClassName("active")[0];
node.textContent = "manual change";

The changes will apply to the DOM. Now you re-render the template with the "old" state:

view.render({ title: "foobar" });

This time the change will not apply. Because the internal cache assumes that the current value is still "foobar" and skips the change.

Basically you have 2 options in this situation:

  1. do not manually change dom properties or states (instead change all through rendering templates)
  2. using the caching helpers which Mikado provides you exactly to this purpose.

Please keep in mind that manual changes to the DOM has its limits:

Generally do not manually change dom properties or states which are not covered by the template. Changes that aren't covered by the template may gets lost when re-rendering (in few situations this will not being an issue).

Caching Helpers

Caching helpers will help you to bypass manual changes to the DOM without going out of sync.

You can also use these helpers for all changes to any DOM node independent of it is part of the template or not. Generally these helpers increase every DOM access.

Set attribute of a node (will not replaces old attributes):

Mikado.setAttribute(node, "href", "/foo");
Mikado.setAttribute(node, {
    id: "foo",
    href: "/foo"
});

Get attribute of a node:

var attr = Mikado.getAttribute(node, "href");

Set class name of a node (fully replaces old classes):

Mikado.setClass(node, "class_a class_b");
Mikado.setClass(node, ["class_a", "class_b"]);

Get class names of a node (returns an array):

var classlist = Mikado.getClass(node);

Set inline styles of a node (fully replaces old styles):

Mikado.setCSS(node, "top: 0; padding-right: 10px");
Mikado.setCSS(node, ["top: 0", "padding-right: 10px"]);

Get all inline styles of a node:

var css = Mikado.getCSS(node);

Set inline styles of a node (will not replaces old styles):

Mikado.setStyle(node, "padding-right", "10px");
Mikado.setStyle(node, {"top": 0, "padding-right": "10px"});

Get a specific inline style of a node:

var style = Mikado.getStyle(node, "padding-right");

Set text of a node:

Mikado.setText(node, "This is a title.");

Get text of a node:

var text = Mikado.getText(node);

Set inner html of a node:

Mikado.setHTML(node, "<b>This is a title.</b>");

Get inner html of a node:

var html = Mikado.getHTML(node);

Storage

Enable internal storage by passing the options during initialization:

var view = new Mikado(template, { store: true });

Whenever you call the .render() function along with passed data, this data will keep in cache (internal storage). Mikado will handle those data for you.

view.render(data);

You can re-render/refresh the last/current state at any time without passing data again:

view.refresh();

Or force an update to a specific index:

view.refresh(index);

Or force an update to a specific node:

view.refresh(node);

Access to the store:

var store = view.store;

Do not de-reference the store, e.g.:

var store = view.store;
// ...
store = [];

Instead do this:

var store = view.store;
// ...
view.store = store = [];

Loose Option

When loose is enabled Mikado will use a data-to-dom binding strategy rather than keeping data separated from rendered elements/templates. This performs generally faster and has lower memory footprint but you will also loose any data at the moment when the corresponding dom element was also removed from the screen (render stack). In most situation this shouldn't be an issue, but it depends on your application.

var view = new Mikado(template, { store: true, loose: true });

To get a data item back from a node you cannot access view.store[] when loose option is enabled. You have to get the item from node or by index:

var item = view.data(index);
var item = view.data(node);

Extern/Custom Store

You can also pass an reference to an external store. This store must be an Array-like type.

var MyStore = [ /* Item Data */ ];

Pass in the external storage when initializing:

var view = new Mikado(root, template, {
    store: MyStore,
    loose: false,
    persist: false
});

Export / Import Views

You can export the data of a view to the local storage.

view.export();

You can import and render the stored data by:

view.import().render();

When exporting/importing templates, the ID is used as key. The template ID corresponds to its filename.

Actually you cannot export several instances of the same template which holds different data. Also the state is not included in the export.

State

State pretty much acts like passing a view payload when rendering templates. State also holds an object but instead used to keep data across runtime. State data are also shared across all Mikado instances. State is directly assigned to each Mikado instance and do not has to pass during rendering. This all differ from using view payloads.

Define state properties:

view.state.date = new Date();
view.state.today = function(){ return view.state.date === new Date() };

You can assign any value as state or function helpers. Do not de-reference the state from the Mikado instance. When using export() the state will just export non-object and non-functional values. You need to re-assign them when the application starts.

Using extern states:

var state = {
    date: new Date(),
    today: function(){ return view.state.date === new Date() }
};

Assign extern states during initialization:

var view = new Mikado(root, template, {
    state: state
});

Transport / Load Templates

Mikado fully supports server-side rendering. The template (including dynamic expressions) will compile to plain compatible JSON.

If your application has a lot of views, you can save memory and performance when loading them at the moment a user has requested this view.

Templates are shared across several Mikado instances.

Load template asynchronously into the global cache:

Mikado.load("https://my-site.com/tpl/template.json", function(error){
    if(error){
        console.error(error);
    }
    else{
        console.log("finished.");
    }
});

Load template asynchronously with Promises into the global cache:

Mikado.load("https://my-site.com/tpl/template.json", true).then(function(){

    console.log("finished.");

}).catch(function(error){

    console.error(error);
});

Load template synchronously by explicit setting the callback to false:

Mikado.load("https://my-site.com/templates/template.json", false);

Assign template to a new Mikado instance, mount and render:

var view = Mikado.new("template");
view.mount(document.body).render(data);

.load() loads and initialize a new template to an existing Mikado instance:

view.load("https://my-site.com/templates/template.json");

.init() assigns a new template to an instance:

view.init("template");

.mount() assigns a new root destination to an instance:

view.mount(document.getElementById("new-root"));

.unload() unloads a template by name (filename):

view.unload("template");

Chain methods:

view.mount(document.body).init("template").render(data);

Static Templates

When a template has no dynamic expressions (within curly brackets) which needs to be evaluated during runtime Mikado will handle those templates as static and skips the dynamic render part. Static views could be rendered without passing data.

Once (One-time rendering)

When a template just needs to be rendered once you can create, mount, render, unload and destroy (full cleanup) as follows:

Mikado.new(template)
      .mount(root)
      .render()
      .unload() // unload before destroy!
      .destroy();

Destroy has a parameter flag to automatically unload before destroy:

Mikado.new(root, template)
      .render()
      .destroy(true);

You can also simply use a shorthand function:

Mikado.once(root, template); // static view
Mikado.once(root, template, data); // dynamic view
Mikado.once(root, template, data, payload, callback);

When destroying a template, template definitions will still remain in the global cache. Maybe for later use or when another instances uses the same template (which is generally not recommended).

When unloading templates explicitly the template will also removes completely. The next time the same template is going to be re-used it has to be re-loaded and re-parsed again. In larger applications it might be useful to unload views to free memory when they was closed by the user.

Compiler Service / Live Templates

Mikado provides you a webserver to serve templates via a simple RESTful API. This allows you to send views live from a server. Also this can be used for live reloading templates in a local development environment.

Install Mikado Server via NPM:

npm install mikado-server

Start the compiler server:

npx mikado server

Instead of npx mikado server you can also use npx mikado-server alternatively.

The service is listening on localhost. The API has this specification:

{host}:{port}/:type/path/to/template.html

Examples:

  • localhost:3000/json/template/app.html
  • localhost:3000/json/template/app (WIP)
  • localhost:3000/template/app.json (WIP)

They all have same semantics, you can use different forms for the same request.

Types:

json Assign them manually via Mikado.register or just render the template once.
es6 Import as an ES6 compatible module.
module A synonym for es6.
es5 Uses Mikado from the global namespace. This requires a non-ES6 build of mikado or import "browser.js", both before loading this template.
js A synonym for es5.

Local Development

The compiler service is also very useful to render templates ony the fly when modifying the source code. Use a flag to switch between development or production environment in your source code, e.g.:

// production:
import tpl_app from "./path/to/app.es6.js";
let app;

if(DEBUG){
    // development:
    Mikado.load("http://localhost:3000/json/path/to/app.html", false);
    app = Mikado.new("app");
}
else{
    app = Mikado.new(tpl_app);
}

// same code follows here ...

You can also import them as ES6 modules directly via an asynchronous IIFE:

let tpl_app;

(async function(){
    if(DEBUG){
        // development:
        tpl_app = await import('http://localhost:3000/es6/path/to/app.html');
    }
    else{
        // production:
        tpl_app = await import("./path/to/app.html");
    }
}());

// same code follows here ...
const app = Mikado.new(tpl_app);

Server-Side Rendering (SSR)

WIP

Use the json format to delegate view data from server to the client. Actually just static templates are supported. An express middleware is actually in progress to create templates with dynamic expressions.

Includes

Partials gets its own instance under the hood. This gains performance and also makes template factories re-usable when same partials are shared across different views.

Be aware of circular includes. A partial cannot include itself (or later in its own chain). Especially when your include-chain growths remember this rule.

Assume you've created a partial template. Make sure the template is providing one single root as the outer bound.

You have to register all partial templates once before you initialize the templates which will including them:

import tpl_header from "./tpl/header.es6.js";
import tpl_article from "./tpl/article.es6.js";
import tpl_footer from "./tpl/footer.es6.js";

Mikado.register(tpl_header);
Mikado.register(tpl_article);
Mikado.register(tpl_footer);

When using templates in ES5 compatible format, they are automatically registered by default. You can also use the runtime compiler and pass the returned template to the register method.

Now you can include partials with a pseudo element:

<section>
    <include>{{ header }}</include>
    <include>{{ article }}</include>
    <include>{{ footer }}</include>
</section>

Use the template name (filename) for includes.

The pseudo element <include> will extract into place and is not a part of the component. You cannot use dynamic expressions within curly brackets, just provide the name of the template.

Equal to:

<section>
    <include from="header"></include>
    <include from="article"></include>
    <include from="footer"></include>
</section>

You can't use self-closing custom elements accordingly to the HTML5 specs e.g. <include from="title"/>.

You can also include to a root node which is part of the component by an attribute:

<section>
    <header include="header"></header>
    <article include="article"></article>
    <footer include="footer"></footer>
</section>

Loop Partials

Assume the template example from above is a tweet (title, article, footer).

<section>
    <title>{{ data.title }}</title>
    <tweets include="tweet" for="data.tweets" max="5">
        <!-- tweet -->
        <!-- tweet -->
        <!-- tweet -->
    </tweets>
</section>

This expression will render the template "tweet" through an array of data items/tweets. The template "tweet" is getting the array value data.tweets as data.

Inline Loops

You can also loop through an inline partial. Mikado will extracting and referencing this partial to its own instance under the hood.

<main>
    <title>{{ data.title }}</title>
    <tweets for="data.tweets">
        <section>
            <header include="header"></header>
            <article include="article"></article>
            <footer include="footer"></footer>
        </section>
    </tweets>
</main>

You can also nest loops:

<tweets for="data.tweets">
    <tweet>
        <h1>{{ data.title }}</h1>
        <title>Comments:</title>
        <div for="data.comments">
            <comment>
                <p>{{ data.content }}</p>
                <title>Replies:</title>
                <div for="data.replies">
                    <p>{{ data.content }}</p>
                </div>
            </comment>
        </div>
    </tweet>
</tweets>

Every looped partial has to provide one single root as the outer bound.

In this example every for-expression is wrong (you will find the right example above):

<tweets for="data.tweets">
    <h1>{{ data.title }}</h1>
    <title>Comments:</title>
    <div for="data.comments">
        <p>{{ data.content }}</p>
        <title>Replies:</title>
        <div for="data.replies">
            {{ data.content }}
        </div>
    </div>
</tweets>

Conditional Branches

<main if="data.tweet.length">
    <title>Tweets: {{ data.tweet.length }}</title>
</main>
<main if="!data.tweet.length">
    <title>No tweets found.</title>
</main>
<main>
    <title>{{ data.title }}</title>
    <tweets if="data.tweets.length" for="data.tweets">
        <section>{{ data.content }}</section>
    </tweets>
</main>
<main>
    <title>{{ data.title }}</title>
    <tweets for="data.tweets">
        <section if="data.content">{{ data.content }}</section>
    </tweets>
</main>

Think in real code branches, instead of doing this:

<main>
    {{@ var result = (function(){
        return "some big computation";
    }()) }}
    <section if="data.content">{{ result }}</section>
</main>

Doing this:

<main>
    <section if="data.content">
        {{ (function(){
            return "some big computation";
        }()) }}
    </section>
</main>

Conditional branches will skip its expressions when not taken.

As well as try to assign computations outside a loop:

<main>
    {{@ var result = data.tweets.length && (function(){
        return "some big computation";
    }()) }}
    <tweets for="data.tweets">
        <section>{{ result }}</section>
    </tweets>
</main>

Reactive Proxy (Observer)

Mikado provides you a reactive way to listen and apply changes of data to the DOM. It is based on the new ES6 proxy feature which gives awesome performance and fully falls back to classical observer when proxy is not available. Using an reactive strategy can additionally boost performance beyond a factor of 100 when updating data. It depends on you application / current view: this feature has an advantage when updating data has to process more often than creating new.

Template markup:

<table>
    <tr>
        <td>Name:</td>
        <td>{{= data.name }}</td>
    </tr>
    <tr>
        <td>Email:</td>
        <td>{{= data.email }}</td>
    </tr>
</table>

The expression for an observable property has to start with: {{=

Use with internal store:

var view = new Mikado(template, { store: true });
view.render([...]);

When data changes, the corresponding dom element will automatically change:

view.store[0].name = "New Name";

Use with external store:

var data = [...];
var view = new Mikado(template, { store: data });
view.render(data);

When data changes, the corresponding dom element will automatically change:

data[0].name = "New Name";
view.store[0].name = "New Name";

Use with loose store:

var view = new Mikado(template, { store: true, loose: true });
view.render([...]);

When data changes, the corresponding dom element will automatically change:

view.data(0).name = "New Name";

Limitations

Proxy actually comes with some limitations on template expressions. Improving those restrictions is already work in progress and will release soon.

1. Fields from deeply nested data objects are not reactive:

var data = {
    id: "foobar", // <-- observable
    content: {    // <-- observable
        title: "title",  // <-- not
        body: "body",    // <-- not
        footer: "footer" // <-- not
    }
};

2. Conditional or advanced template expressions are not supported:

<table>
    <tr>
        <td>Name:</td>
        <!-- Supported: -->
        <td>{{= data.name }}</td>
    </tr>
    <tr>
        <td>Tweets:</td>
        <!-- Not Supported: -->
        <td>{{= data.tweets ? data.tweets.length : 0 }}</td>
    </tr>
</table>

Just use plain property notation within curly brackets.

Bind Input Elements

WIP (release when reaching 2500 Github stars)

The attribute bind provides you a 2-way-binding of input elements with your data store.

<main>
    <input type="text" bind="{{ data.name }}">
    <input type="checkbox" bind="{{ data.status }}">
    <input type="radio" value="female" bind="{{ data.gender }}">
    <input type="radio" value="male" bind="{{ data.gender }}">
</main>

When data is changed, the input elements will automatically update, as well as other turn around, when the input elements gets new data the store will automatically update.

Best Practices

A Mikado instance has a stronger relation to the template as to the root element. Please keep this example in mind:

This is good:

var view = new Mikado(template);

view.mount(root_a).render(data);
view.mount(root_b).render(data);
view.mount(root_c).render(data);

This is okay, but instead of this:

view.mount(root);
view.init(tpl_a).render(data);
view.init(tpl_b).render(data);
view.init(tpl_c).render(data);

Doing this:

var view_a = new Mikado(tpl_a);
var view_b = new Mikado(tpl_b);
var view_c = new Mikado(tpl_c);

view_a.mount(root_c).render(data);
view_b.mount(root_b).render(data);
view_c.mount(root_a).render(data);

Ideally every template should have initialized by one Mikado instance and should be re-mounted when using in another context. Re-mounting is very fast but re-assigning templates is not as fast.

Memory Optimizations

Clear shared pools of the current template:

view.purge();

Clear cache:

view.sync(/* uncache? */ true);

Destroy a view:

view.destroy();

Unload/unregister a template definition:

view.unload();

Destroy a view + unload:

view.destroy(/* unload? */ true);

Concept of Shared Pools

The are four kinds of synchronized pools under the hood. Three of them are shared across all template instances to make them re-usable. That also save memory and skip redundant re-calculations.


Mikado Shared Pool (Concept)

Factory Pool

The factory pool shares partials or same template definitions. When partials or templates are used more than once they will point to the same instance. That will save memory, skip redundant re-calculations and also improve runtime execution, because different jobs can now run through the same process (less reference spread).

Template Pool

The template pool is a feature accordingly to the option reuse and extends the strategy of re-using. Templates has to be created by the factory just once and stay available for re-using along the whole runtime. That's drastically improves everything to a complete new level. The memory allocation cuts down to a minimum (approx. reduce by a factor of 10). The creation/instantiation of new templates gets an amazing boost. When removing instances the garbage collector has almost nothing to do.

Keyed Pool

The keyed pool is basically the same concept like template pool, but it has keyed access and works differently than the template pool (which is queued and has indexed access). The keyed pool and the template pool are cross-shared, both of them could access the other. It depends on the mode which is running (keyed, non-keyed).

Live Pool

The live pool contains all elements which are actually rendered on screen (in use). That will keep track of not sharing elements which are already in use by another view. When elements were removed, they will move from live pool to the shared pools. When the option reuse was set to false, the live pool will also share its elements to the next render loop of the same view.

Motivation

This library was build by reversed engineering with these primary goals as its base:

  1. providing a clean, simple and non-cryptic tool for developers who focus on living standards and common styles
  2. designer-readable templates based on pure html (most famous and compatible markup in the web)
  3. providing the best overall performance
  4. can be flexibly integrated into every stack

Milestones

There are some features in draft and it points out that those features requires more effort to implement. I need some motivation, so I will wait for this library gets more popular.

1000 Github Stars Providing pseudo attributes else and elseif within templates to chain conditionals.
2500 Github Stars New attribute bind for input elements (reactive 2-way binding of input elements).
5000 Github Stars Express middleware / standalone webserver for high performance bi-directional server-side rendering (SSR).
10k Github Stars A reboot of Xone on top of Mikado. Xone is a full-stack mobile application SDK which seamlessly runs in browser and also supports Cordova and NativeScript from the same codebase.

Custom Builds

Perform a full build:

npm run build

Perform a light build:

npm run build:light

Perform a custom Build:

npm run build:custom ENABLE_CACHE=false LANGUAGE_OUT=ECMASCRIPT5 USE_POLYFILL=true

On custom builds each build flag will be set to false by default.

The custom build will be saved to dist/mikado.custom.xxxxx.js (the "xxxxx" is a hash based on the used build flags).

The destination folder of the build is: /dist/

Supported Build Flags
Flag Values Info
DEBUG true, false Log debugging infos
SUPPORT_CACHE true, false DOM Cache
SUPPORT_EVENTS true, false Template event bindings
SUPPORT_STORAGE true, false Template data binding
SUPPORT_HELPERS true, false, string DOM Manipulation helpers (supports comma separated string)
SUPPORT_CACHE_HELPERS true, false DOM Cache helpers
SUPPORT_ASYNC true, false Asynchronous rendering (Promise Support)
SUPPORT_TRANSPORT true, false Load templates through the network
SUPPORT_TEMPLATE_EXTENSION true, false Use loops, includes and conditionals within templates
SUPPORT_REACTIVE true, false Use reactive data binding
SUPPORT_COMPILE true, false Use runtime template compiler

Compiler Flags
USE_POLYFILL true, false Include Polyfills (based on Ecmascript 5)
LANGUAGE_OUT







ECMASCRIPT3
ECMASCRIPT5
ECMASCRIPT5_STRICT
ECMASCRIPT6
ECMASCRIPT6_STRICT
ECMASCRIPT_2015
ECMASCRIPT_2017
STABLE
Target language

Copyright 2019 Nextapps GmbH
Released under the Apache 2.0 License

You can’t perform that action at this time.