/mikado

Web's smartest templating engine.

Primary LanguageJavaScriptApache License 2.0Apache-2.0


Mikado.js

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

Getting Started  •  Options  •  API  •  Mikado CLI  •  Custom Builds  •  Template Compiler  •  Template Server  •  Express Middleware (SSR)

Services:

Benchmark:

Demo:

  1. Basic Example (Classic Javascript)
  2. Basic Example (ES6 Modules)
  3. TodoMVC App
  4. js-framework-benchmark

Take a look into the source code of this pages is the best starting point.

Work in progress:

  • Conditional branches
  • Includes/partials
  • Live templates (for local development)
  • Persistent state
  • Express Middleware
  • Paginated Render
  • Loops (through partials)
  • Plugin API

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

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

Node.js

npm install mikado

Feature Comparison

Feature mikado.min.js mikado.light.js
Template Engine
VDOM Cache
Event Binding -
Data Binding -
Persistent Storage -
Transport/Load Templates -
Export/Import Views -
VDOM Manipulation Helpers -
Conditional Branches -
Includes/Partials -
File Size (gzip) 4.1 kb 1.6 kb

Benchmark (Stress Test)

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

Rank Library Size kB Memory B Create Update Partial Append Repaint Reduce Remove Score
1 mikado-0.1.2 2 167336 1599 7158 12829 838 251559 28938 25969 945
2 domc-0.0.12 4.46 207488 957 6575 11284 865 92368 20701 24041 704
3 sinuous-0.14.2 7.48 265192 384 9695 11996 384 308152 17639 13464 626
4 redom-3.24.1 2.88 246936 369 2632 4790 381 58468 19843 11991 446
5 inferno-7.3.1 8.4 410960 706 2623 3199 617 5538 6527 14009 344
7 innerHTML - 406992 983 956 854 425 847 1742 26346 343
6 surplus-0.5.3 15.79 166288 698 632 640 282 639 1460 15469 294
8 jquery-3.4.1 31.26 819032 746 650 578 305 565 895 4919 158

More results are coming soon.

Score = Sumtest(self_ops / max_ops) / test_count * 1000

The maximum possible score is 1000, that requires a library to be best in each category. This stress test is based on a real life use case, where you will fetch new data from a source and do computations on it. Libraries cannot use all their tricks for internal state caching, it measures the real workload of a real use case.

API Overview

Constructor:

Global methods:

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

Instance methods:

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

Instance properties:

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.
async Perform render tasks asynchronously and return a Promise. false
cache Enable/disable caching. Caching can greatly increase performance (up to 20x). Be careful, it fully depends on your application, there are situations where a enabled cache performs slower.
Recommendation: enable caching when several of your item data will stay unchanged from one to another render task. Disable caching when changes on data requires a fully re-render more often.
false
store Passed items for rendering are also stored and synchronized along the virtual dom. You can re-render the full state at any time, without passing the item 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 item data whenever a corresponding dom element was removed. When set to true you cannot use paged rendering. 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. { }
once Performs the render of a template just one time. This only applies on static views. The render ist starting immediately (do not call .render again!). When finishing it fully cleans up (removes view, item data and also the template definition). This is useful for static views, which should persist in the app. false

Basic Example

Install Mikado via NPM or include one of the distributed builds:

npm install mikado

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

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>{{item.user}}</td>
    </tr>
    <tr>
        <td>Tweets:</td>
        <td>{{item.tweets.length}}</td>
    </tr>
</table>

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

The preserved keyword item is a reference to a passed 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:

mikado compile template/template.html

Notation: mikado {{input}} {{destination}}

Instead of 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)

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

var items = [{
    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="template/template.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 "./template/template.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 data:

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

You can also chain methods:

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

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.

Advanced Example

A bit more complex template:

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

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

Within a template you have access to the following indentifiers:

Identifier Description Passed Mode
item A full reference to a passed data item. auto
view An optional payload used to manually pass in non-item specific data or helper functions. manual
index Represents the index of the currently rendered item. auto
this Provides you access to the Mikado view instance. auto
this.state An object used to keep data as a state across runtime. State data are shared across all Mikado instances. auto (manual set)
this.store Gives access to the internal item store (Array). auto
this.length The length of all items actually rendered (to get length of stored items use this.store.length instead). auto
window The global namespace auto
self Points to the current rendered element itself. Using "js" node property or by using the {{@ marker grants you to have "self" available. auto

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 item, view, index and this.state you can also access variables from the global namespace.

For performance relevant tasks avoid passing html contents as string.

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

var items = [{
    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 data (non-item specific data and helper methods used by the template):

var view = {
    page: 1,
    today: "2019-01-11",
    parseFooter: function(item){ return item.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(items, view);

Render asynchronously by providing a callback function:

view.render(items, view, 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(items, view).then(function(){
    console.log("finished.");
});

Event Bindings

Lets take this example:

<table data-id="{{item.id}}" root>
    <tr>
        <td>User:</td>
        <td click="show-user">{{item.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){

    alert(node.textContent);

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

    alert(node.dataset.id);
})

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

List of all native supported events:

  • 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.
swipe * This gesture is currently in progress.

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 (Assign Data)

When using storage, every render task also updates the storage data.

Render a template incrementally through a set of item data:

view.render(items);

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

view.render(items, view);

Schedule a render task asynchronously to the next animation frame:

view.render(items, view, true);

Schedule a render task by using a callback:

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

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

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

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

view.render();

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

view.refresh();

Repaint the current state on a specific index:

view.refresh(index);

Modify Views

Add one item to the end:

view.add(items);

Add one item before an index:

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

Append multiple items to the end:

view.append(items);

Append multiple items before an index:

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

Remove a specific item/node:

view.remove(node);

Remove first 20 items:

view.remove(20);

Remove last 20 items:

view.remove(-20);

Remove next 20 items of a given node (including):

view.remove(node, 20);

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

view.remove(node, -20);

Remove all:

view.clear();

Replace an item/node:

view.replace(old, new);

Update an single item/node:

view.update(node, item);

Re-Sync Virtual DOM:

view.sync();

Useful Helpers

Get a node from the virtual DOM by index:

var node = view.node(index);

Get an item from the store by index:

var item = view.item(index);

Get the current index from a node:

var index = view.index(node);

Manipulate Views

You can decide to just one of these:

  1. manipulate the DOM directly or
  2. use the builtin-methods for those purposes

Using the builtin-methods have the best performance.

Do not mix manual changes to the DOM with builtin-methods, because manually changes will made the virtual DOM cache out of sync.

Move an item/node to a specific index:

view.move(node, 15);

Shift an item/node by a specific offset:

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

Move an item/node to the top or bottom:

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

Move an item/node by 1 index:

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

Move an item/node before or after another item/node:

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

Swap two items/nodes:

view.swap(node_a, node_b);

Shuffle items/nodes:

view.shuffle();

Sort items/nodes by a field from the item data:

view.sort("title");

Sort items/nodes by descending order:

view.sort("title", "desc");

Sort items/nodes by a custom handler (should return negative, positive and zero offsets):

view.sort(function(item_a, item_b){
    return item_a.time < item_b.time ? 1 : 
          (item_a.time > item_b.time ? -1 : 0)
});

Storage

Enable storage by passing the options during initialization:

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

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

view.render(items);

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

view.render();

Or force an update to a specific index:

view.update(index);

Export / Import Views

You can export the latest rendering state of a view along with its item data to the local storage.

view.export();

You can import and render the stored view by:

view.import().render();

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

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

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 item 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. When enabled you cannot use paginated render.

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

Changes to the DOM may automatically change your data to keep the state in sync.

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

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

Chain methods:

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

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 item data.

When a template just needs to be rendered once you can create, mount, render. unload, destroy to fully cleanup as follows:

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

Or use an option flag as a shorthand:

Mikado.new(root, template, { once: true });

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 destroy also dynamic views when it was closed by the user to free memory.

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:

mikado server

Instead of 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 via Mikado.register or just render the template once.
es6 Import as ES6 compatible modules.
js / es5 Uses Mikado from the global namespace. This requires a non-ES6 build of mikado or import "browser.js", both before loading this template.

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";
import tpl_view from "./path/to/view.es6.js";

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

// same code follows here ...

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

let tpl_app, tpl_view;

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

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

Server-Side Rendering (SSR)

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. When using SSR it may be useless to keep the template data as well as calculate all the optimizations for re-using. For this purpose use the method Mikado.once().

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(items);
view.mount(root_b).render(items);
view.mount(root_c).render(items);

This is bad:

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

Instead 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(items);
view_b.mount(root_b).render(items);
view_c.mount(root_a).render(items);

Ideally every template should have initialized by one (and only 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.

Includes

Partials gets its own instance under the hood. This results in high performance and also makes template factories re-usable when sharing same partials 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.

You can include partials as follows:

<section>
    <title include="title"></title>
    <article include="article" as="item.content"></article>
    <footer include="footer"></footer>
</section>

The include attribute is related to the template name (filename), the as attribute is the reference which should be passed as the item to the partial.

Please notice, that each template requires one single root. When the template "template/title" has multiple nodes in the outer bound then wrap this into a new element as root or include as follows:

<section>
    <include>{{ template/title }}</include>
    <include as="item.content">{{ template/article }}</include>
    <include>{{ template/footer }}</include>
</section>

Use pseudo-element:

<section>
    <include from="title"/>
    <include from="article" as="item.content"/>
    <include from="footer"/>
</section>

The pseudo-element <include> will extract into place. You cannot use dynamic expressions within curly brackets, just provide the name of the template.

In this example the template "template/title" gets the tag <title> as the template route.

Loop Partials

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

In this example the template "tweet" loops the render through an array of tweets. The template "tweet" will get the array value from the current index as item.

Inline Loops

<main>
    <title>{{ item.title }}</title>
    <tweets for="item.tweets">
        <section>{{ item.content }}</section>
    </tweets>
</main>

Conditional Branches

<main if="item.tweet.length">
    <title>{{ item.title }}</title>
    <section>{{ item.content }}</section>
    <footer>{{ item.footer }}</footer>
</main>
<main if="item.contacts.length">
    <title>{{ item.title }}</title>
    <section>{{ item.content }}</section>
    <footer>{{ item.footer }}</footer>
</main>
<main>
    <title>{{ item.title }}</title>
    <tweets if="item.tweets.length" for="item.tweets">
        <section>{{ item.content }}</section>
    </tweets>
</main>
<main>
    <title>{{ item.title }}</title>
    <tweets for="item.tweets">
        <section if="item.content">{{ item.content }}</section>
    </tweets>
</main>

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 VDOM Cache
SUPPORT_EVENTS true, false Template event bindings
SUPPORT_STORAGE true, false Template data binding
SUPPORT_HELPERS true, false VDOM Manipulation helpers
SUPPORT_ASYNC true, false Asynchronous rendering (Promise Support)
SUPPORT_TRANSPORT true, false Load templates through the network

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