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 v0.8.0 Revision is Work in Progress!
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:
- Basic Example + Runtime Compiler (HTML5 Template)
- Basic Example + Runtime Compiler (String Template)
- Basic Example + Events (ES5)
- Basic Example + Events (ES6 Modules)
- Basic Example + Events (Development Sources)
- TodoMVC App: Source Codeβ/βRun Demo
- 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
- Get Latest
- Feature Comparison: Mikado Light
- Benchmark Ranking (Rendering Performance)
- API Overview
- Options
- Template Compiler
- Rules and Conventions
- Basic Example
- Advanced Example
- Event Bindings
- Keyed / Non-Keyed Modes
- Non-Reusing
- Usage:
- DOM State Caching
- Stores:
- State
- Callbacks
- Transport / Load Templates
- Static Templates
- Compiler Service / Live Templates
- Template Features:
- Reactive Proxy (Observer)
- Best Practices
- Memory Optimizations
- About Reconcile (Diffing)
- Concept of Shared Pools
- 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:
- new Mikado(<root>, template, <options>) : view
Global methods:
- Mikado(<root>, template, <options>) : view
- Mikado.once(root, template, <data>, <payload>, <callback>)
- Mikado.register(template)
- Mikado.unregister(template)
Global methods (not included in mikado.light.js):
- Mikado.compile(<templateβ|βstring>)
- Mikado.load(url, <callback>)
- Mikado.unload(template)
- mikado.route(name, handler, <options>)
- mikado.listen(event, <options>)
- mikado.unlisten(event, <options>)
- mikado.dispatch(name, <target>, <event>, <self>)
Instance methods:
- view.init(<template>, <options>)
- view.mount(root)
- view.render(<data>, <payload>, <callback>)
- view.reconcile(data)
- view.create(data, <payload>)
- view.add(data, <payload>, <index>)
- view.append(data, <payload>, <index>)
- view.update(nodeβ|βindex, data, <payload>)
- view.replace(nodeβ|βindex, data, <payload>)
- view.remove(node, <count>)
- view.clear()
- view.data(indexβ|βnode)
- view.node(index)
- view.index(node)
- view.destroy(<unload?>)
- view.unload()
Instance methods (not included in mikado.light.js):
- view.refresh(<nodeβ|βindex>, <payload>)
- view.sync(<uncache?>)
- view.purge()
- view.find(data)
- view.search(data)
- view.where(payload)
- view.import()
- view.export()
- view.load(url, <callback>)
- view.route(name, handler, <options>)
- view.listen(event, <options>)
- view.unlisten(event, <options>)
- view.dispatch(name, <target>, <event>, <self>)
DOM manipulation helpers (optional, not included in mikado.light.js):
- view.move(nodeβ|βindex, index)
- view.shift(nodeβ|βindex, index)
- view.up(nodeβ|βindex)
- view.down(nodeβ|βindex)
- view.first(nodeβ|βindex)
- view.last(nodeβ|βindex)
- view.before(nodeβ|βindex, nodeβ|βindex)
- view.after(nodeβ|βindex, nodeβ|βindex)
- view.swap(nodeβ|βindex, nodeβ|βindex)
Instance properties:
view.dom- view.length
- view.store
- view.state
view.configview.template
Global helpers (optional, not included in mikado.light.js):
- Mikado.setText(node, text)
- Mikado.getText(node)
- Mikado.setHTML(node, html)
- Mikado.getHTML(node)
- Mikado.setClass(node, class)
- Mikado.getClass(node)
- Mikado.hasClass(node, class)
- Mikado.removeClass(node, class)
- Mikado.toggleClass(node, class)
Mikado.setStyle(node, property, value)Mikado.getStyle(node, property- Mikado.setCSS(node, css)
- Mikado.getCSS(node)
- Mikado.setAttribute(node, attr, value)
- Mikado.getAttribute(node, attr)
- Mikado.hasAttribute(node, attr)
- Mikado.removeAttribute(node, attr)
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) |
|
Compiler Service (Server) |
|
HTML5 Templates (Runtime) |
|
Template String (Runtime) |
|
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:
- template.js the template compiled in ES5 compatible Javascript
- template.es6.js the template compiled as an ES6 module
- template.json the template compiled in JSON-compatible notation (to load via HTTP request)
- 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();
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
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);
view.clear();
view.replace(old, new);
Update a single data item/node:
view.update(node, data);
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);
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:
- do not manually change dom properties or states which are part of a template expression (instead change all through rendering templates)
- using the caching helpers which Mikado provides you exactly to this purpose.
- 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"
});
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");
Mikado.setText(node, "This is a title.");
var text = Mikado.getText(node);
Mikado.setHTML(node, "<b>This is a title.</b>");
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.
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:
- The queued pool extends the feature of "recycling" (reusing) nodes
- 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:
- providing a clean, simple and non-cryptic tool for developers who focus on living standards and common styles
- designer-readable templates based on pure HTML (most famous and compatible markup in the web)
- providing the best overall performance
- 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