• Stars
    star
    731
  • Rank 61,995 (Top 2 %)
  • Language
    JavaScript
  • License
    Apache License 2.0
  • Created about 5 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Mikado is the webs fastest template library for building user interfaces.

Mikado v0.8.0 Revision is Work in Progress!

A bunch of improvements will be coming which was adressed over 3 years of using Mikado in production applications. Targeting simpler usage and integration, more template capabilities, advanced support for the reactive paradigm, a rework of the pooling strategy, enhanced template compiler which is almost fully compatible to the current generation, and of course tons of improvements to make this library even more robust. Further updates are coming soon.

Mikado - Webs fastest templating engine

Modern template engine based on living standards. 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, non-keyed and also reactive paradigm switchable out of the box. Let's start building the next generation of high-performance applications.

Getting Started  ‒  Options  ‒  API  ‒  Concept  ‒  Benchmark  ‒  Custom Builds  ‒  Template Compiler  ‒  Template Server  ‒  Changelog

Benchmark:

Demo:

  1. Basic Example + Runtime Compiler (HTML5 Template)
  2. Basic Example + Runtime Compiler (String Template)
  3. Basic Example + Events (ES5)
  4. Basic Example + Events (ES6 Modules)
  5. Basic Example + Events (Development Sources)
  6. TodoMVC App: Source Code / Run Demo
  7. js-framework-benchmark: keyed / non-keyed / keyed (proxy)

First Steps

Mikado is based on living standards and uses a similar templating notation style like "mustache" or "handlebars". You do not have to learn a new language, you just need some basic skills you already have. It will take 3 minutes to become productive. Don't let that confuse you with the size of this documentation, because it will show you a lot of in-depth details. You will do not need these details to start with. If you would like to know more you get a chance to go deeper.

Also, all compiled dist files will work out of the box, no TypeScript, no Webpack, no module loader, no external tools are required.

Guide for new developers (the most simple example, just takes 3 minutes):

  • Load this bundle through a script tag resource
  • Provide a basic template as native HTML5 template
  • Compile the template, then create a Mikado instance by passing in the compiled template and mount the root to this new created instance
    • var view = new Mikado(template).mount(root);
  • Just use view.render(data) over and over for all jobs: add / remove / clear / update / reconcile / ...
  • Final Source Code

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. Stores:
  16. State
  17. Callbacks
  18. Transport / Load Templates
  19. Static Templates
  20. Compiler Service / Live Templates
  21. Template Features:
  22. Reactive Proxy (Observer)
  23. Best Practices
  24. Memory Optimizations
  25. About Reconcile (Diffing)
  26. Concept of Shared Pools
  27. Custom Builds

Get Latest

Bundle

Choose one of these bundles:

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.es5.js Download https://rawcdn.githack.com/nextapps-de/mikado/master/dist/mikado.es5.js
mikado.debug.js Download https://rawcdn.githack.com/nextapps-de/mikado/master/dist/mikado.debug.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.6.6/, or also a commit hash.

The es5-strict version includes all features. The debug version additionally provides debugging information through the console.

Example:

<script src="dist/mikado.min.js"></script>
<script>
  // ....
</script>

Node.js

Install Mikado via NPM:

npm install mikado

The dist and src folders are located in node_modules/mikado/.

ES6 Modules

Production

The ES6 minified production modules are located in dist/module/.

<script>
  import Mikado from "./dist/module/mikado.js";
</script>

You can also load modules via CDN, e.g.:

<script>
  import Mikado from "https://unpkg.com/[email protected]/dist/module/mikado.js";
</script>

Development

Use the modules from the "src" folder for development/debugging. When using the "src" modules you have to load the "src/config.js" via a normal script tag before you load the modules.

<script src="src/config.js"></script>
<script>
  import Mikado from "./src/mikado.js";
</script>

Feature Comparison

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

Benchmark Ranking (Rendering Performance)

Run the benchmark (recycle):
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

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

Keyed

Library KB RAM Create Replace Update Order Repaint Add Remove Toggle Clear Index Score
mikado 3 22 19301 8535 206747 51470 220010 35346 27945 31265 26378 996 54089
mikado-proxy 8.3 30 10288 5901 27129 18648 28194 14912 19278 16526 26216 537 12803
solid 0 339 737 665 7291 4029 13279 1391 7487 2470 15227 149 3587
inferno 8.4 311 754 724 5493 5266 6055 1323 7443 2302 15982 191 2647
mithril 9.6 263 637 612 4599 4267 4997 1120 6614 2004 12622 170 2256
redom 2.9 522 421 411 4146 3719 4215 761 5750 1380 11744 190 1954
domc 4.5 393 1078 1059 1082 1129 1101 1128 2049 1464 24931 211 1250
innerhtml 0 494 1029 999 993 876 885 935 1769 1186 27131 154 1107
surplus 15.8 626 975 857 849 854 846 878 1560 1187 23713 162 987
sinuous 7.5 650 842 809 812 824 820 813 1577 1096 18047 159 941
jquery 31.3 684 809 707 703 643 652 698 1129 860 5520 84 708
lit-html 17.3 1179 441 411 409 413 409 431 761 550 4964 79 487
ractive 68.2 4684 165 156 158 158 159 166 298 212 1944 36 202
knockout 24.8 2657 91 67 68 68 68 84 130 103 1162 45 161

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

Read more about this test and also show ranking table for "non-keyed" and "data-driven" here.

API Overview

Most of these methods are optional, 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 "External Store").
false
loose When store 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 Set it to true to use both pools: Recycle Pool + Keyed Pool (autoscale), or set it to false to fully disable pooling, or set it to either one of both: "queue" or "key" to enable just one of them respectively. true
size Sets the maximum size of the shared pool. When not set or false it uses "auto scaling". false
prefetch Prefetch/prebuilt a template on page load. Disable to save memory a speed up page start. true

Compile Templates

Compiler Methods

Method Notes
Mikado Compiler (CLI)
  • instant performance
  • recommended for production
  • bundle templates easily out of the box
Compiler Service (Server)
  • server-side
  • good for production
  • bundle not required
  • best caching/re-using capabilities
  • enable live updates
HTML5 Templates (Runtime)
  • requires compiling during runtime
  • good for development
  • bundle templates requires additional tooling (like webpack)
Template String (Runtime)
  • requires compiling during runtime
  • compiling strings comes with an extra cost
  • good for development
  • bundle templates easily out of the box

Note: 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 ("mikado-compile") 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.

Compile the template through the command line by:

npx mikado-compile user/list.html

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

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(document.getElementById("user-list"));

Alternatively (supports just templates/elements with IDs):

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

Create a mikado view:

var view = new Mikado(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 a mikado view:

var view = new Mikado(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("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(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:

Mikado(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 the same root is not possible:

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

This may be 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 does not have 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 better performance, relevant tasks avoid passing HTML contents as a string.

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

Within a template you have access to the following identifiers:

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 "view.parseFooter"). Alternatively you can also nest more complex computations inline as an IIFE and return the result.

<div class="date">
  {{ (function(){ 
    var date = new Date();
    // perform some code ...
    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 from above you need one single object or an array of data items:

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(template, { async: true });

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

Event Bindings

Let's 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.

Event Options

By default, every event which is delegated to a route will be canceled (event.preventDefault) and also will stop capturing/bubbling (event.stopPropagation). To control this behavior you can configure for each route:

Mikado.route("handler", function(target, event){
  console.log("Clicked");
},{
  cancel: false,
  stop: false 
});

cancel prevents default behavior for this event (default: "true")
stop stop capturing/bubbling the event after matched (default: "true")

Explicit Register/Unregister

You can also use the event delegation along with "routes" outside a template. Just apply the event attribute as 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 events once:

Mikado.listen("click");

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

The default event capture option flag would be set to false by default. When you need to configure event capturing and passive listener just do:

Please make sure this call runs before passing the template for creating a new mikado instance.

Mikado.listen("touchmove", {
    passive: true,
    capture: true 
});

Same way you could also unregister events:

Mikado.unlisten("click");

Dispatch Event Handler

Manually dispatch an event:

view.dispatch("handler");

Manually dispatch an event and pass parameters:

view.dispatch("handler", target, event, self);

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 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(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-Pool)

A keyed strategy limits the reusability of components based on items with the same ID. It just requires a 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(template, { reuse: false, pool: false });

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

3. Explicit Keyed (Shared Pool)

This is a special mode that 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(template, { reuse: false, pool: "key" });

This will switch Mikado into an "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 provides 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(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(template, { pool: "key" });

This will switch Mikado into an "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 be integrated into every stack, especially when additional UI libs where used.

Mikado can restore 100% of the original state. This helps in situations where:

  • external libraries change components nodes
  • event listeners were bound directly to components nodes
  • external data/values were referenced to components nodes
  • components nodes were 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 vs. Reconcile

Take advantage of Mikados 3 different render functions. Especially when reusing was disabled, this gives you full control.

.refresh() Just apply the data changes to the DOM. (did not add/remove/move)
.reconcile() Just apply item order by moving nodes along the shortest path. (did not add/remove/update)
.render() Perform a full update. (including: add/remove/reconcile/update)

The render function is already trying to apply the minimum required changes to the DOM. But prediction is always limited, also nothing could make a prediction better than the developer itself who is implementing the task. Most of the optional methods provided by Mikado are simply just there, to get the most out when it matters. Use them to manual control the flow of batch processes and optimize computation-heavy tasks.

Whenever you call .render() when also reusing was explicitly 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 make 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 a 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 component root node or an index to the refresh method performs faster than passing no parameter.

Hint: You cannot use refresh when new items were added/removed, this requires .render(data).

When you just want to move items to its new order without updating its contents (also no add/remove) and you are in the keyed mode you can call reconciliation directly:

view.reconcile(items);

Hint: The sum of .reconcile(data) and .refresh() is basically .render(data) under the hood. When you need both: adding/removing/moving together with updating contents than call .render(data) instead of calling the corresponding partial methods one by one.

Create, Initialize, Destroy Views

Create a view from a template with options:

var view = Mikado(template, options);

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

var view = Mikado(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 store (not external), every render task also updates the stored 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 a store to be enabled):

view.refresh();

Repaint the current state on a specific index:

view.refresh(index);

Just create a 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 and 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 a 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 the same content (that may require each data item to be unique, otherwise use where):

var node = view.search(data);

Find all nodes which match a given payload (will always return an array, empty if no results were 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 a 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 by applying manual changes Mikado provides you several optional helper functions to manipulate the dom and also keep them in sync. Using the helper function also grants you a maximum performance.

All helpers could be used by index or by node as passed parameters.

Helpers let you apply simple transformations without running through the whole render roundtrip of the full template. Super-advanced-fine-grained reconciliation isn't the holy grail, it is just for your laziness.

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 a 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 the future, so best is to explicitly set this flag when initializing:

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

We strongly recommended reading 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 the title:

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

This time, the property will be 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, text contents, etc) you just wrap this as an expression within the template and it will apply automatically.

For example, when you would like to also change the classname, 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.

You have 2 options in this situation:

  1. do not manually change dom properties or states which are part of a template expression (instead change all through rendering templates)
  2. using the caching helpers which Mikado provides you exactly to this purpose.
  3. using view.sync() as a fallback

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

Generally, do not manually change dom properties or states which are part of a template expression. Changes that aren't covered by the template may get lost when re-rendering (this must not be an issue). Also use "keyed" mode to keep your changes on the corresponding template entry (disables recycling).

Caching Helpers (optional)

Caching helpers let you bypass manual changes to the DOM easily 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 replace 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 replace 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);

Stores

Mikado provides 4 different types of stores. It is very useful to understand how they are processed internally.

1. Internal Store

An internal store gets updated automatically by Mikado. This comes with a small extra cost. Use this store when you need a reference to the data store as an array of items that are currently rendered.

When the internal store is used, this store gets automatically updated by any of Mikados methods e.g. render/update/add/append/remove.

Enable internal store bypassing the options during initialization:

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

Whenever you call the .render() function along with passed data, this data will be updated (add/remove/change) to the internal store.

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 = [];

2. Loose Store (Default)

When loose is enabled Mikado will use a data-to-dom binding strategy rather than keeping data separated from rendered elements/templates. This performs slightly faster and has a lower memory footprint but you will also lose any data at the moment when the corresponding dom element was also removed from the screen/dom (render stack). Often, this is the expected behavior, but it depends on your application.

Initialize a loose store:

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

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

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

3. External/Custom Store

External stores differ from the other stores. An external store assumes to get updated from the outside and will not be changed by Mikado. That means that you have to apply all changes to the external store before rendering. Use this store when:

  • you like to use data-driven style
  • you need sharing the data store to your application functions or libs
  • you like to make the data store immutable for Mikado

When an external store is used, this store gets not updated by any of Mikados methods e.g. render/update/add/append/remove.

There is one exception: when you use a proxy (observable attributes), the external store will be replaced by the proxied reference once (otherwise the proxy feature becomes useless).

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

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

Pass in the external store when initializing:

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

4. Reactive Store (Observable Array)

This is also an external store with all its attributes described above. Additionally, this store reacts when indices get changed (applies changes to DOM automatically). That makes reconciliation unnecessary but also has a noticeable extra cost for all other kinds of updates. The main reason why this store is slower in the benchmark by a large margin is, that this store cannot apply a bulk of updates through a loop. It reacts at the moment the data was assigned/removed from an index. Still, this store could perform faster than all other ones depending on your application / current view.

The reactive store could also be used in a combination with the proxy feature. Using both provides you a complete reactive store where you did not need calling any of Mikados methods again like render/reconcile/update/add/append/remove. All these methods gets redundant/obsolete because the view is completely synchronized along with the whole state of your store. This combination and how they are integrated into Mikado are unique. The "repaint" test from the benchmark ist just an empty function call and performs astronomical.

Read the documentation about this kind of store here.

Export / Import Views

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

view.export();

You can import and render the stored data by:

view.import().render();

When exporting/importing templates, the ID is used as a 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.

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. The state is directly assigned to each Mikado instance and does not have 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
});

Callbacks

Apply callbacks during initialization:

var view = new Mikado(root, template, {
  on: {
    create: function(node) {
      console.log("created:", node);
    },
    insert: function(node) {
      console.log("inserted:", node);
    },
    update: function(node) {
      console.log("updated:", node);
    },
    change: function(node) {
      console.log("changed:", node);
    },
    remove: function(node) {
      console.log("removed:", node);
    }
  }
});
Callback Description
create Called when a new template node was created.
add Called when a new template node was added.
update Called when a template node was updated.
change Called when the contents of a template node has changed.
remove Called when a template node was removed.

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 the template to a new Mikado instance, mount and render:

var view = Mikado("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 need 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(template)
  .mount(root)
  .render()
  .unload() // unload before destroy!
  .destroy();

Destroy has a parameter flag to automatically unload before destroy:

Mikado(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 remain in the global cache. Maybe for later use or when another instance uses the same template (which is generally not recommended).

When unloading templates explicitly the template will also remove 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 were closed by a 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

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 the 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 "bundle.js", both before loading this template.
js A synonym for es5.

Local Development

The compiler service is also very useful to render templates on 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("app");
} else {
  app = Mikado(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(tpl_app);

Server-Side Rendering (SSR)

WIP

Use the JSON format to delegate view data from the server to the client. 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 performance gain also makes template factories re-usable when the 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 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">
    <!-- 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.

The max attribute could be used optionally to limit the partial loop:

<tweets include="tweet" for="data.tweets" max="5"></tweets>

The max attribute could also be negative to reverse the boundary direction, e.g. loop through the last 5 items:

<tweets include="tweet" for="data.tweets" max="-5"></tweets>

Inline Loops

You can also loop through an inline partial. Mikado will extract 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 their expressions when not taken.

As well as try to assign computations outside a loop:

<main>
  {{@ var result = (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 great performance and fully falls back to a classical observer when the proxy is not available. Using a reactive strategy can additionally boost performance beyond a factor of 100 when updating data. It depends on your 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: {{=

Using proxy requires using one of the 3 store strategies.

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

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

3. 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 comes with some limitations on template expressions. Removing these 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.

Stealth Mode

Whenever all your template expressions are just using proxy notation it enables the "stealth" mode, which boosts performance from every update process to the absolute maximum. This mode has no advantage when every render loop has to apply new items.

This enables stealth mode:

<item>
  <caption>
    Name:
  </caption>
  <p>{{= data.name }}</p>
  <caption>
    Email:
  </caption>
  <p>{{= data.mail }}</p>
</item>

This not:

<item>
  <caption>
    Name:
  </caption>
  <p>{{= data.name }}</p>
  <caption>
    Email:
  </caption>
  <p>{{ data.mail }}</p>
</item>

Also using conditionals, loops and inline javascript will prevent switching to the stealth mode. Just includes (without loop) could be used additionally to the proxy notation, but it requires all fields also observed by the partial which is included.

Observable Array (Virtual NodeList)

Additionally to react on changes of properties you can create an observable Array that acts like a synchronized NodeList. It uses ES6 Proxy under the hood which fully falls back to the classical observer, when not available.

Semantically the observable Array is equal to an array-like Javascript array.

Create an observable array:

var array = Mikado.array();

Create an observable array with initial data:

var items = [...];
var array = Mikado.array(items);

Bind this store to a Mikado instance:

var view = Mikado(target, template, { store: array });

Now the observable array is linked with your instance. Whenever you change the array all changes apply automatically to the corresponding template.

You can use all common array built-ins, e.g.:

array.push({ ... });
var last = array.pop();
array.unshift({ ... });
array.splice(0, 1, { ... });

The best option is to get and set via array index access which is a rarely available feature (including non-proxy fallback):

array[0] = { ... };
array[array.length] = { ... };
var first = array[0];

A list of all supported array prototypes:

  • length
  • push
  • pop
  • shift
  • unshift
  • slice
  • splice
  • concat
  • indexOf
  • lastIndexOf
  • filter
  • map
  • reverse
  • sort
  • swap

These methods are implemented, without some extensions like parameter chaining. They may come in a future update .e.g array.push(a, b, c) is not available, instead, you have to call push for each item on by one.

The method array.swap(a, b) is an optional performance shortcut.

There are some methods which differ slightly from the original implementation. These methods will apply changes in place and returning the original reference instead of applying on a copy:

  • concat
  • filter
  • map

When you need the original behavior you can simply do that by:

var new_array = [ ... ];
var copy = Array.prototype.concat.call(array, new_array);
var copy = Array.prototype.filter.call(array, function(){ ... });

There is a limitation when falling back to the non-proxy polyfill. You cannot fill sparse arrays or access indexes which are greater than the current array.length. There is just one undefined index that could always accessed (by read/write) that is the last "undefined" index on an array when you call array[array.length]. This index is a special marker that increases the "virtual" array size. Whenever you assign a value to this special index the size of the observable index growth automatically and the next "undefined" index in the queue becomes this marker. This limitation is not existing when the ES6 proxy is available.

Also, there are some drawbacks when reflection is used:

var array = Mikado.array();
console.log(array.constructor === Array); // -> false
console.log(array.prototype === Array.prototype); // -> false
console.log(array instanceof Array); // -> false

The proxy feature theoretically allows those reflections but could not be used to keep the polyfill working in addition to sharing most of the same codebase.

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

Reconcile (Diffing)

Mikado comes with its own new diffing algorithm which gains performance of reconciling/re-arrangement. The algorithm is based on the "Longest Distance" concept which was invented by me, the author of this library. I also discovered two other concepts from scratch from where I have also implemented the "3-Way-Splice", but the longest distance has slightly better overall performance. Although by a very small margin. Theoretically, the splice concept has some advantages but it isn't that easy to make them capable.

Mikados reconcile provides you the most effective diffing today (you can take the row "order" from the benchmark as a reference).

Concept of Shared Pools

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


Mikado Shared Pool (Concept)

Factory Pool

The factory pool shares partials or the 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 have to be created by the factory just once and stay available for reuse along the whole runtime.

Keyed Pool

The keyed pool is basically the same concept as the 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 synchronized. It depends on the options which were set.

Live Pool

The live pool contains all elements which are rendered on-screen (in use). That will keep track of not sharing elements that are already in use by another view. When elements were removed, they will move from the 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.

Some notes about pools

Pooling just extends concepts which already exist/used:

  1. The queued pool extends the feature of "recycling" (reusing) nodes
  2. The keyed pool extends the feature of keeping components which are referential keyed

Motivation

This library was built 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

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