From aee13901d83429ac5173d052db28ff30183f0434 Mon Sep 17 00:00:00 2001 From: Leo Date: Thu, 26 Jan 2017 20:46:32 -0500 Subject: [PATCH] docs tweaks --- .eslintignore | 1 + archive/v1.0.0-rc.8/animation.html | 139 ++++ archive/v1.0.0-rc.8/api.html | 176 +++++ archive/v1.0.0-rc.8/autoredraw.html | 153 ++++ archive/v1.0.0-rc.8/buildQueryString.html | 110 +++ archive/v1.0.0-rc.8/change-log.html | 510 ++++++++++++ archive/v1.0.0-rc.8/components.html | 420 ++++++++++ archive/v1.0.0-rc.8/contributing.html | 112 +++ archive/v1.0.0-rc.8/credits.html | 90 +++ archive/v1.0.0-rc.8/css.html | 137 ++++ archive/v1.0.0-rc.8/es6.html | 158 ++++ archive/v1.0.0-rc.8/examples.html | 76 ++ archive/v1.0.0-rc.8/fragment.html | 135 ++++ archive/v1.0.0-rc.8/framework-comparison.html | 258 +++++++ archive/v1.0.0-rc.8/guides.html | 66 ++ archive/v1.0.0-rc.8/hyperscript.html | 425 ++++++++++ archive/v1.0.0-rc.8/index.html | 254 ++++++ archive/v1.0.0-rc.8/installation.html | 222 ++++++ archive/v1.0.0-rc.8/jsonp.html | 165 ++++ archive/v1.0.0-rc.8/jsx.html | 245 ++++++ archive/v1.0.0-rc.8/keys.html | 176 +++++ archive/v1.0.0-rc.8/layout.html | 30 + archive/v1.0.0-rc.8/lib/prism/prism.css | 6 + archive/v1.0.0-rc.8/lib/prism/prism.js | 7 + archive/v1.0.0-rc.8/lifecycle-methods.html | 208 +++++ archive/v1.0.0-rc.8/methods.html | 57 ++ archive/v1.0.0-rc.8/mount.html | 132 ++++ archive/v1.0.0-rc.8/parseQueryString.html | 122 +++ archive/v1.0.0-rc.8/promise.html | 449 +++++++++++ archive/v1.0.0-rc.8/redraw.html | 95 +++ archive/v1.0.0-rc.8/releasing.html | 86 +++ archive/v1.0.0-rc.8/render.html | 125 +++ archive/v1.0.0-rc.8/request.html | 537 +++++++++++++ archive/v1.0.0-rc.8/route.html | 730 ++++++++++++++++++ archive/v1.0.0-rc.8/signatures.html | 117 +++ archive/v1.0.0-rc.8/simple-application.html | 540 +++++++++++++ archive/v1.0.0-rc.8/stream.html | 589 ++++++++++++++ archive/v1.0.0-rc.8/style.css | 64 ++ archive/v1.0.0-rc.8/testing.html | 122 +++ archive/v1.0.0-rc.8/trust.html | 211 +++++ archive/v1.0.0-rc.8/version.html | 92 +++ archive/v1.0.0-rc.8/vnodes.html | 215 ++++++ archive/v1.0.0-rc.8/withAttr.html | 190 +++++ docs/change-log.md | 1 + docs/components.md | 12 +- docs/framework-comparison.md | 16 +- docs/generate.js | 15 +- docs/guides.md | 2 +- docs/{introduction.md => index.md} | 40 +- docs/layout.html | 4 +- docs/lint.js | 29 +- docs/style.css | 6 +- 52 files changed, 8830 insertions(+), 47 deletions(-) create mode 100644 archive/v1.0.0-rc.8/animation.html create mode 100644 archive/v1.0.0-rc.8/api.html create mode 100644 archive/v1.0.0-rc.8/autoredraw.html create mode 100644 archive/v1.0.0-rc.8/buildQueryString.html create mode 100644 archive/v1.0.0-rc.8/change-log.html create mode 100644 archive/v1.0.0-rc.8/components.html create mode 100644 archive/v1.0.0-rc.8/contributing.html create mode 100644 archive/v1.0.0-rc.8/credits.html create mode 100644 archive/v1.0.0-rc.8/css.html create mode 100644 archive/v1.0.0-rc.8/es6.html create mode 100644 archive/v1.0.0-rc.8/examples.html create mode 100644 archive/v1.0.0-rc.8/fragment.html create mode 100644 archive/v1.0.0-rc.8/framework-comparison.html create mode 100644 archive/v1.0.0-rc.8/guides.html create mode 100644 archive/v1.0.0-rc.8/hyperscript.html create mode 100644 archive/v1.0.0-rc.8/index.html create mode 100644 archive/v1.0.0-rc.8/installation.html create mode 100644 archive/v1.0.0-rc.8/jsonp.html create mode 100644 archive/v1.0.0-rc.8/jsx.html create mode 100644 archive/v1.0.0-rc.8/keys.html create mode 100644 archive/v1.0.0-rc.8/layout.html create mode 100644 archive/v1.0.0-rc.8/lib/prism/prism.css create mode 100644 archive/v1.0.0-rc.8/lib/prism/prism.js create mode 100644 archive/v1.0.0-rc.8/lifecycle-methods.html create mode 100644 archive/v1.0.0-rc.8/methods.html create mode 100644 archive/v1.0.0-rc.8/mount.html create mode 100644 archive/v1.0.0-rc.8/parseQueryString.html create mode 100644 archive/v1.0.0-rc.8/promise.html create mode 100644 archive/v1.0.0-rc.8/redraw.html create mode 100644 archive/v1.0.0-rc.8/releasing.html create mode 100644 archive/v1.0.0-rc.8/render.html create mode 100644 archive/v1.0.0-rc.8/request.html create mode 100644 archive/v1.0.0-rc.8/route.html create mode 100644 archive/v1.0.0-rc.8/signatures.html create mode 100644 archive/v1.0.0-rc.8/simple-application.html create mode 100644 archive/v1.0.0-rc.8/stream.html create mode 100644 archive/v1.0.0-rc.8/style.css create mode 100644 archive/v1.0.0-rc.8/testing.html create mode 100644 archive/v1.0.0-rc.8/trust.html create mode 100644 archive/v1.0.0-rc.8/version.html create mode 100644 archive/v1.0.0-rc.8/vnodes.html create mode 100644 archive/v1.0.0-rc.8/withAttr.html rename docs/{introduction.md => index.md} (80%) diff --git a/.eslintignore b/.eslintignore index a3726b93..1b9fe34c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ test-utils ospec mithril.js mithril.min.js +archive diff --git a/archive/v1.0.0-rc.8/animation.html b/archive/v1.0.0-rc.8/animation.html new file mode 100644 index 00000000..c049a765 --- /dev/null +++ b/archive/v1.0.0-rc.8/animation.html @@ -0,0 +1,139 @@ + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Animations

+ +
+

Technology choices

+

Animations are often used to make applications come alive. Nowadays, browsers have good support for CSS animations, and there are various libraries that provide fast Javascript-based animations. There's also an upcoming Web API and a polyfill if you like living on the bleeding edge.

+

Mithril does not provide any animation APIs per se, since these other options are more than sufficient to achieve rich, complex animations. Mithril does, however, offer hooks to make life easier in some specific cases where it's traditionally difficult to make animations work.

+
+

Animation on element creation

+

Animating an element via CSS when the element created couldn't be simpler. Just add an animation to a CSS class normally:

+
.fancy {animation:fade-in 0.5s;}
+@keyframes fade-in {
+    from {opacity:0;}
+    to {opacity:1;}
+}
+
+
var FancyComponent = {
+    view: function() {
+        return m(".fancy", "Hello world")
+    }
+}
+
+m.mount(document.body, FancyComponent)
+
+
+

Animation on element removal

+

The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril offers a onbeforeremove hook that allows us to defer the removal of an element.

+

Let's create an exit animation that fades opacity from 1 to 0.

+
.exit {animation:fade-out 0.5s;}
+@keyframes fade-out {
+    from {opacity:1;}
+    to {opacity:0;}
+}
+
+

Now let's create a contrived component that shows and hides the FancyComponent we created in the previous section:

+
var on = true
+
+var Toggler = {
+    view: function() {
+        return [
+            m("button", {onclick: function() {on = !on}}, "Toggle"),
+            on ? m(FancyComponent) : null,
+        ]
+    }
+}
+
+

Next, let's modify FancyComponent so that it fades out when removed:

+
var FancyComponent = {
+    onbeforeremove: function(vnode) {
+        vnode.dom.classList.add("exit")
+        return new Promise(function(resolve) {
+            setTimeout(resolve, 500)
+        })
+    },
+    view: function() {
+        return m(".fancy", "Hello world")
+    }
+}
+
+

vnode.dom points to the root DOM element of the component (<div class="fancy">). We use the classList API here to add an exit class to <div class="fancy">.

+

Then we return a Promise that resolves after half a second. When we return a promise from onbeforeremove, Mithril waits until the promise is resolved and only then it removes the element. In this case, it waits half a second, giving the exit animation the exact time it needs to complete.

+

We can verify that both the enter and exit animations work by mounting the Toggler component:

+
m.mount(document.body, Toggler)
+
+

Note that the onbeforeremove hook only fires on the element that loses its parentNode when an element gets detached from the DOM. This behavior is by design and exists to prevent a potential jarring user experience where every conceivable exit animation on the page would run on a route change. If your exit animation is not running, make sure to attach the onbeforeremove handler as high up the tree as it makes sense to ensure that your animation code is called.

+
+

Performance

+

When creating animations, it's recommended that you only use the opacity and transform CSS rules, since these can be hardware-accelerated by modern browsers and yield better performance than animating top, left, width, and height.

+

It's also recommended that you avoid the box-shadow rule and selectors like :nth-child, since these are also resource intensive options. Other things that can be expensive include large or dynamically scaled images and overlapping elements with different position values (e.g. an absolute postioned element over a fixed element).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

API

+ +

Cheatsheet

+

Here are examples for the most commonly used methods. If a method is not listed below, it's meant for advanced usage.

+

m(selector, attrs, children) - docs

+
m("div.class#id", {title: "title"}, ["children"])
+
+
+

m.mount(element, component) - docs

+
var state = {
+    count: 0,
+    inc: function() {state.count++}
+}
+
+var Counter = {
+    view: function() {
+        return m("div", {onclick: state.inc}, state.count)
+    }
+}
+
+m.mount(document.body, Counter)
+
+
+

m.route(root, defaultRoute, routes) - docs

+
var Home = {
+    view: function() {
+        return "Welcome"
+    }
+}
+
+m.route(document.body, "/home", {
+    "/home": Home, // defines `http://localhost/#!/home`
+})
+
+

m.route.set(path) - docs

+
m.route.set("/home")
+
+

m.route.get() - docs

+
var currentRoute = m.route.get()
+
+

m.route.prefix(prefix) - docs

+

Call this before m.route()

+
m.route.prefix("#!")
+
+ +
m("a[href='/Home']", {oncreate: m.route.link}, "Go to home page")
+
+
+

m.request(options) - docs

+
m.request({
+    method: "PUT",
+    url: "/api/v1/users/:id",
+    data: {id: 1, name: "test"}
+})
+.then(function(result) {
+    console.log(result)
+})
+
+
+

m.jsonp(options) - docs

+
m.jsonp({
+    url: "/api/v1/users/:id",
+    data: {id: 1},
+    callbackKey: "callback",
+})
+.then(function(result) {
+    console.log(result)
+})
+
+
+

m.parseQueryString(querystring) - docs

+
var object = m.parseQueryString("a=1&b=2")
+// {a: "1", b: "2"}
+
+
+

m.buildQueryString(object) - docs

+
var querystring = m.buildQueryString({a: "1", b: "2"})
+// "a=1&b=2"
+
+
+

m.withAttr(attrName, callback) - docs

+
var state = {
+    value: "",
+    setValue: function(v) {state.value = v}
+}
+
+var Component = {
+    view: function() {
+        return m("input", {
+            oninput: m.withAttr("value", state.setValue),
+            value: state.value,
+        })
+    }
+}
+
+m.mount(document.body, Component)
+
+
+

m.trust(htmlString) - docs

+
m.render(document.body, m.trust("<h1>Hello</h1>"))
+
+
+

m.redraw() - docs

+
var count = 0
+function inc() {
+    setInterval(function() {
+        count++
+        m.redraw()
+    }, 1000)
+}
+
+var Counter = {
+    oninit: inc,
+    view: function() {
+        return m("div", count)
+    }
+}
+
+m.mount(document.body, Counter)
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

The auto-redraw system

+ +

Mithril implements a virtual DOM diffing system for fast rendering, and in addition, it offers various mechanisms to gain granular control over the rendering of an application.

+

When used idiomatically, Mithril employs an auto-redraw system that synchronizes the DOM whenever changes are made in the data layer. The auto-redraw system becomes enabled when you call m.mount or m.route (but it stays disabled if your app is bootstrapped solely via m.render calls).

+

The auto-redraw system simply consists of triggering a re-render function behind the scenes after certain functions complete.

+

After event handlers

+

Mithril automatically redraws after DOM event handlers that are defined in a Mithril view:

+
var MyComponent = {
+    view: function() {
+        return m("div", {onclick: doSomething})
+    }
+}
+
+function doSomething() {
+    // a redraw happens synchronously after this function runs
+}
+
+m.mount(document.body, MyComponent)
+
+

You can disable an auto-redraw for specific events by setting e.redraw to false.

+
var MyComponent = {
+    view: function() {
+        return m("div", {onclick: doSomething})
+    }
+}
+
+function doSomething(e) {
+    e.redraw = false
+    // no longer triggers a redraw when the div is clicked
+}
+
+m.mount(document.body, MyComponent)
+
+

After m.request

+

Mithril automatically redraws after m.request completes:

+
m.request("/api/v1/users").then(function() {
+    // a redraw happens after this function runs
+})
+
+

You can disable an auto-redraw for a specific request by setting the background option to true:

+
m.request("/api/v1/users", {background: true}).then(function() {
+    // does not trigger a redraw
+})
+
+

After route changes

+

Mithril automatically redraws after m.route.set() calls (or route changes via links that use m.route.link

+
var RoutedComponent = {
+    view: function() {
+        return [
+            // a redraw happens asynchronously after the route changes
+            m("a", {href: "/", oncreate: m.route.link}),
+            m("div", {
+                onclick: function() {
+                    m.route.set("/")
+                }
+            }),
+        ]
+    }
+}
+
+m.route(document.body, "/", {
+    "/": RoutedComponent,
+})
+
+
+

When Mithril does not redraw

+

Mithril does not redraw after setTimeout, setInterval, requestAnimationFrame and 3rd party library event handlers (e.g. Socket.io callbacks). In those cases, you must manually call m.redraw().

+

Mithril also does not redraw after lifecycle methods. Parts of the UI may be redrawn after an oninit handler, but other parts of the UI may already have been redrawn when a given oninit handler fires. Handlers like oncreate and onupdate fire after the UI has been redrawn.

+

If you need to explicitly trigger a redraw within a lifecycle method, you should call m.redraw(), which will trigger an asynchronous redraw.

+
var StableComponent = {
+    oncreate: function(vnode) {
+        vnode.state.height = vnode.dom.offsetHeight
+        m.redraw()
+    },
+    view: function() {
+        return m("div", "This component is " + vnode.state.height + "px tall")
+    }
+}
+
+

Mithril does not auto-redraw vnode trees that are rendered via m.render. This means redraws do not occur after event changes and m.request calls for templates that were rendered via m.render. Thus, if your architecture requires manual control over when rendering occurs (as can sometimes be the case when using libraries like Redux), you should use m.render instead of m.mount.

+

Remember that m.render expects a vnode tree, and m.mount expects a component:

+
// wrap the component in a m() call for m.render
+m.render(document.body, m(MyComponent))
+
+// don't wrap the component for m.mount
+m.mount(document.body, MyComponent)
+
+

Mithril may also avoid auto-redrawing if the frequency of requested redraws is higher than one animation frame (typically around 16ms). This means, for example, that when using fast-firing events like onresize or onscroll, Mithril will automatically throttle the number of redraws to avoid lag.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

buildQueryString(object)

+ +
+

Description

+

Turns an object into a string of form a=1&b=2

+
var querystring = m.buildQueryString({a: "1", b: "2"})
+// "a=1&b=2"
+
+
+

Signature

+

querystring = m.buildQueryString(object)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
objectObjectYesA key-value map to be converted into a string
returnsStringA string representing the input object
+

How to read signatures

+
+

How it works

+

The m.buildQueryString creates a querystring from an object. It's useful for manipulating URLs

+
var querystring = m.buildQueryString({a: 1, b: 2})
+
+// querystring is "a=1&b=2"
+
+

Deep data structures

+

Deep data structures are serialized in a way that is understood by popular web application servers such as PHP, Rails and ExpressJS

+
var querystring = m.buildQueryString({a: ["hello", "world"]})
+
+// querystring is "a[0]=hello&a[1]=world"
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Change log

+ +
+

Migrating from v0.2.x

+

v1.x is largely API-compatible with v0.2.x, but there are some breaking changes.

+

If you are migrating, consider using the mithril-codemods tool to help automate the most straightforward migrations.

+ +
+

m.prop removed

+

In v1.x, m.prop() is now a more powerful stream micro-library, but it's no longer part of core. You can read about how to use the optional Streams module in the documentation.

+

v0.2.x

+
var m = require("mithril")
+
+var num = m.prop(1)
+
+

v1.x

+
var m = require("mithril")
+var prop = require("mithril/stream")
+
+var num = prop(1)
+var doubled = num.map(function(n) {return n * 2})
+
+
+

m.component removed

+

In v0.2.x components could be created using either m(component) or m.component(component). v1.x only supports m(component).

+

v0.2.x

+
// These are equivalent
+m.component(component)
+m(component)
+
+

v1.x

+
m(component)
+
+
+

config function

+

In v0.2.x mithril provided a single lifecycle method, config. v1.x provides much more fine-grained control over the lifecycle of a vnode.

+

v0.2.x

+
m("div", {
+    config : function(element, isInitialized) {
+        // runs on each redraw
+        // isInitialized is a boolean representing if the node has been added to the DOM
+    }
+})
+
+

v1.x

+

More documentation on these new methods is available in lifecycle-methods.md.

+
m("div", {
+    // Called before the DOM node is created
+    oninit : function(vnode) { /*...*/ },
+    // Called after the DOM node is created
+    oncreate : function(vnode) { /*...*/ },
+    // Called before the node is updated, return false to cancel
+    onbeforeupdate : function(vnode, old) { /*...*/ },
+    // Called after the node is updated
+    onupdate : function(vnode) { /*...*/ },
+    // Called before the node is removed, return a Promise that resolves when
+    // ready for the node to be removed from the DOM
+    onbeforeremove : function(vnode) { /*...*/ },
+    // Called before the node is removed, but after onbeforeremove calls done()
+    onremove : function(vnode) { /*...*/ }
+})
+
+

If available the DOM-Element of the vnode can be accessed at vnode.dom.

+
+

Changes in redraw behaviour

+

Mithril's rendering engine still operates on the basis of semi-automated global redraws, but some APIs and behaviours differ:

+

No more redraw locks

+

In v0.2.x, Mithril allowed 'redraw locks' which temporarily prevented blocked draw logic: by default, m.request would lock the draw loop on execution and unlock when all pending requests had resolved - the same behaviour could be invoked manually using m.startComputation() and m.endComputation(). The latter APIs and the associated behaviour has been removed in v1.x. Redraw locking can lead to buggy UIs: the concerns of one part of the application should not be allowed to prevent other parts of the view from updating to reflect change.

+

Cancelling redraw from event handlers

+

m.mount() and m.route() still automatically redraw after a DOM event handler runs. Cancelling these redraws from within your event handlers is now done by setting the redraw property on the passed-in event object to false.

+

v0.2.x

+
m("div", {
+    onclick : function(e) {
+        m.redraw.strategy("none")
+    }
+})
+
+

v1.x

+
m("div", {
+    onclick : function(e) {
+        e.redraw = false
+    }
+})
+
+

Synchronous redraw removed

+

In v0.2.x it was possible to force mithril to redraw immediately by passing a truthy value to m.redraw(). This behavior complicated usage of m.redraw() and caused some hard-to-reason about issues and has been removed.

+

v0.2.x

+
m.redraw(true) // redraws immediately & synchronously
+
+

v1.x

+
m.redraw() // schedules a redraw on the next requestAnimationFrame tick
+
+

m.startComputation/m.endComputation removed

+

They are considered anti-patterns and have a number of problematic edge cases, so they no longer exist in v1.x.

+
+

Component controller function

+

In v1.x there is no more controller property in components, use oninit instead.

+

v0.2.x

+
m.mount(document.body, {
+    controller : function() {
+        var ctrl = this
+
+        ctrl.fooga = 1
+    },
+
+    view : function(ctrl) {
+        return m("p", ctrl.fooga)
+    }
+})
+
+

v1.x

+
m.mount(document.body, {
+    oninit : function(vnode) {
+        vnode.state.fooga = 1
+    },
+
+    view : function(vnode) {
+        return m("p", vnode.state.fooga)
+    }
+})
+
+// OR
+
+m.mount(document.body, {
+    oninit : function(vnode) {
+        var state = this  // this is bound to vnode.state by default
+
+        state.fooga = 1
+    },
+
+    view : function(vnode) {
+        var state = this // this is bound to vnode.state by default
+
+        return m("p", state.fooga)
+    }
+})
+
+
+

Component arguments

+

Arguments to a component in v1.x must be an object, simple values like String/Number/Boolean will be treated as text children. Arguments are accessed within the component by reading them from the vnode.attrs object.

+

v0.2.x

+
var component = {
+    controller : function(options) {
+        // options.fooga === 1
+    },
+
+    view : function(ctrl, options) {
+        // options.fooga == 1
+    }
+}
+
+m("div", m.component(component, { fooga : 1 }))
+
+

v1.x

+
var component = {
+    oninit : function(vnode) {
+        // vnode.attrs.fooga === 1
+    },
+
+    view : function(vnode) {
+        // vnode.attrs.fooga == 1
+    }
+}
+
+m("div", m(component, { fooga : 1 }))
+
+
+

view() parameters

+

In v0.2.x view functions are passed a reference to the controller instance and (optionally) any options passed to the component. In v1.x they are passed only the vnode, exactly like the controller function.

+

v0.2.x

+
m.mount(document.body, {
+    controller : function() {},
+
+    view : function(ctrl, options) {
+        // ...
+    }
+})
+
+

v1.x

+
m.mount(document.body, {
+    oninit : function(vnode) {
+        // ...
+    },
+
+    view : function(vnode) {
+        // Use vnode.state instead of ctrl
+        // Use vnode.attrs instead of options
+    }
+})
+
+
+

Passing components to m()

+

In v0.2.x you could pass components as the second argument of m() w/o any wrapping required. To help with consistency in v1.x they must always be wrapped with a m() invocation.

+

v0.2.x

+
m("div", component)
+
+

v1.x

+
m("div", m(component))
+
+
+

Passing vnodes to m.mount() and m.route()

+

In v0.2.x, m.mount(element, component) tolerated vnodes as second arguments instead of components (even though it wasn't documented). Likewise, m.route(element, defaultRoute, routes) accepted vnodes as values in the routes object.

+

In v1.x, components are required instead in both cases.

+

v0.2.x

+
m.mount(element, m('i', 'hello'))
+m.mount(element, m(Component, attrs))
+
+m.route(element, '/', {
+    '/': m('b', 'bye')
+})
+
+

v1.x

+
m.mount(element, {view: function () {return m('i', 'hello')}})
+m.mount(element, {view: function () {return m(Component, attrs)}})
+
+m.route(element, '/', {
+    '/': {view: function () {return m('b', 'bye')}}
+})
+
+
+

m.route.mode

+

In v0.2.x the routing mode could be set by assigning a string of "pathname", "hash", or "search" to m.route.mode. In v.1.x it is replaced by m.route.prefix(prefix) where prefix can be #, ?, or an empty string (for "pathname" mode). The new API also supports hashbang (#!), which is the default, and it supports non-root pathnames and arbitrary mode variations such as querybang (?!)

+

v0.2.x

+
m.route.mode = "pathname"
+m.route.mode = "search"
+
+

v1.x

+
m.route.prefix("")
+m.route.prefix("?")
+
+
+

m.route() and anchor tags

+

Handling clicks on anchor tags via the mithril router is similar to v0.2.x but uses a new lifecycle method and API.

+

v0.2.x

+
// When clicked this link will load the "/path" route instead of navigating
+m("a", {
+    href   : "/path",
+    config : m.route
+})
+
+

v1.x

+
// When clicked this link will load the "/path" route instead of navigating
+m("a", {
+    href     : "/path",
+    oncreate : m.route.link
+})
+
+
+

Reading/writing the current route

+

In v0.2.x all interaction w/ the current route happened via m.route(). In v1.x this has been broken out into two functions.

+

v0.2.x

+
// Getting the current route
+m.route()
+
+// Setting a new route
+m.route("/other/route")
+
+

v1.x

+
// Getting the current route
+m.route.get()
+
+// Setting a new route
+m.route.set("/other/route")
+
+
+

Accessing route params

+

In v0.2.x reading route params was entirely handled through m.route.param(). This API is still available in v1.x, and additionally any route params are passed as properties in the attrs object on the vnode.

+

v0.2.x

+
m.route(document.body, "/booga", {
+    "/:attr" : {
+        controller : function() {
+            m.route.param("attr") // "booga"
+        },
+        view : function() {
+            m.route.param("attr") // "booga"
+        }
+    }
+})
+
+

v1.x

+
m.route(document.body, "/booga", {
+    "/:attr" : {
+        oninit : function(vnode) {
+            vnode.attrs.attr // "booga"
+            m.route.param("attr") // "booga"
+        },
+        view : function(vnode) {
+            vnode.attrs.attr // "booga"
+            m.route.param("attr") // "booga"
+        }
+    }
+})
+
+
+

Preventing unmounting

+

It is no longer possible to prevent unmounting via onunload's e.preventDefault(). Instead you should explicitly call m.route.set when the expected conditions are met.

+

v0.2.x

+
var Component = {
+    controller: function() {
+        this.onunload = function(e) {
+            if (condition) e.preventDefault()
+        }
+    },
+    view: function() {
+        return m("a[href=/]", {config: m.route})
+    }
+}
+
+

v1.x

+
var Component = {
+    view: function() {
+        return m("a", {onclick: function() {if (!condition) m.route.set("/")}})
+    }
+}
+
+
+

m.request

+

Promises returned by m.request are no longer m.prop getter-setters. In addition, initialValue, unwrapSuccess and unwrapError are no longer supported options.

+

In addition, requests no longer have m.startComputation/m.endComputation semantics. Instead, redraws are always triggered when a request promise chain completes (unless background:true is set).

+

v0.2.x

+
var data = m.request({
+    method: "GET",
+    url: "https://api.github.com/",
+    initialValue: [],
+})
+
+setTimeout(function() {
+    console.log(data())
+}, 1000)
+
+

v1.x

+
var data = []
+m.request({
+    method: "GET",
+    url: "https://api.github.com/",
+})
+.then(function (responseBody) {
+    data = responseBody
+})
+
+setTimeout(function() {
+    console.log(data) // note: not a getter-setter
+}, 1000)
+
+

Additionally, if the extract option is passed to m.request the return value of the provided function will be used directly to resolve the request promise, and the deserialize callback is ignored.

+
+

m.deferred removed

+

v0.2.x used its own custom asynchronous contract object, exposed as m.deferred, which was used as the basis for m.request. v1.x uses Promises instead, and implements a polyfill in non-supporting environments. In situations where you would have used m.deferred, you should use Promises instead.

+

v0.2.x

+
var greetAsync = function() {
+    var deferred = m.deferred()
+    setTimeout(function() {
+        deferred.resolve("hello")
+    }, 1000)
+    return deferred.promise
+}
+
+greetAsync()
+    .then(function(value) {return value + " world"})
+    .then(function(value) {console.log(value)}) //logs "hello world" after 1 second
+
+

v1.x

+
var greetAsync = new Promise(function(resolve){
+    setTimeout(function() {
+        resolve("hello")
+    }, 1000)
+})
+
+greetAsync()
+    .then(function(value) {return value + " world"})
+    .then(function(value) {console.log(value)}) //logs "hello world" after 1 second
+
+
+

m.sync removed

+

Since v1.x uses standards-compliant Promises, m.sync is redundant. Use Promise.all instead.

+

v0.2.x

+
m.sync([
+    m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
+    m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }),
+])
+.then(function (users) {
+    console.log("Contributors:", users[0].name, "and", users[1].name)
+})
+
+

v1.x

+
Promise.all([
+    m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
+    m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }),
+])
+.then(function (users) {
+    console.log("Contributors:", users[0].name, "and", users[1].name)
+})
+
+
+ +

In v0.2.x, the xlink namespace was the only supported attribute namespace, and it was supported via special casing behavior. Now namespace parsing is fully supported, and namespaced attributes should explicitly declare their namespace.

+

v0.2.x

+
m("svg",
+    // the `href` attribute is namespaced automatically
+    m("image[href='image.gif']")
+)
+
+

v1.x

+
m("svg",
+    // User-specified namespace on the `href` attribute
+    m("image[xlink:href='image.gif']")
+)
+
+
+

Nested arrays in views

+

Arrays now represent fragments, which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays.

+
+

vnode equality checks

+

If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains more detail on this issue.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Components

+ +

Structure

+

Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.

+

Any Javascript object that has a view method is a Mithril component. Components can be consumed via the m() utility:

+
var Example = {
+    view: function() {
+        return m("div", "Hello")
+    }
+}
+
+m(Example)
+
+// equivalent HTML
+// <div>Hello</div>
+
+
+

Passing data to components

+

Data can be passed to component instances by passing an attrs object as the second parameter in the hyperscript function:

+
m(Example, {name: "Floyd"})
+
+

This data can be accessed in the component's view or lifecycle methods via the vnode.attrs:

+
var Example = {
+    view: function (vnode) {
+        return m("div", "Hello, " + vnode.attrs.name)
+    }
+}
+
+

NOTE: Lifecycle methods can also be provided via the attrs object, so you should avoid using the lifecycle method names for your own callbacks as they would also be invoked by Mithril. Use lifecycle methods in attrs only when you specifically wish to create lifecycle hooks.

+
+

Lifecycle methods

+

Components can have the same lifecycle methods as virtual DOM nodes: oninit, oncreate, onupdate, onbeforeremove, onremove and onbeforeupdate.

+
var ComponentWithHooks = {
+    oninit: function(vnode) {
+        console.log("initialized")
+    },
+    oncreate: function(vnode) {
+        console.log("DOM created")
+    },
+    onupdate: function(vnode) {
+        console.log("DOM updated")
+    },
+    onbeforeremove: function(vnode) {
+        console.log("exit animation can start")
+        return new Promise(function(resolve) {
+            // call after animation completes
+            resolve()
+        })
+    },
+    onremove: function(vnode) {
+        console.log("removing DOM element")
+    },
+    onbeforeupdate: function(vnode, old) {
+        return true
+    },
+    view: function(vnode) {
+        return "hello"
+    }
+}
+
+

Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.

+
function initialize() {
+    console.log("initialized as vnode")
+}
+
+m(ComponentWithHooks, {oninit: initialize})
+
+

Lifecycle methods in vnodes do not override component methods, nor vice versa. Component lifecycle methods are always run after the vnode's corresponding method.

+

Take care not to use lifecycle method names for your own callback function names in vnodes.

+

To learn more about lifecycle methods, see the lifecycle methods page.

+
+

State

+

Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns.

+

The state of a component can be accessed three ways: as a blueprint at initialization, via vnode.state and via the this keyword in component methods.

+

At initialization

+

Any property attached to the component object is copied for every instance of the component. This allows simple state initialization.

+

In the example below, data is a property of the ComponentWithInitialState component's state object.

+
var ComponentWithInitialState = {
+    data: "Initial content",
+    view: function(vnode) {
+        return m("div", vnode.state.data)
+    }
+}
+
+m(ComponentWithInitialState)
+
+// Equivalent HTML
+// <div>Initial content</div>
+
+

Via vnode.state

+

State can also be accessed via the vnode.state property, which is available to all lifecycle methods as well as the view method of a component.

+
var ComponentWithDynamicState = {
+    oninit: function(vnode) {
+        vnode.state.data = vnode.attrs.text
+    },
+    view: function(vnode) {
+        return m("div", vnode.state.data)
+    }
+}
+
+m(ComponentWithDynamicState, {text: "Hello"})
+
+// Equivalent HTML
+// <div>Hello</div>
+
+

Via the this keyword

+

State can also be accessed via the this keyword, which is available to all lifecycle methods as well as the view method of a component.

+
var ComponentUsingThis = {
+    oninit: function(vnode) {
+        this.data = vnode.attrs.text
+    },
+    view: function(vnode) {
+        return m("div", this.data)
+    }
+}
+
+m(ComponentUsingThis, {text: "Hello"})
+
+// Equivalent HTML
+// <div>Hello</div>
+
+

Be aware that when using ES5 functions, the value of this in nested anonymous functions is not the component instance. There are two recommended ways to get around this Javascript limitation, use ES6 arrow functions, or if ES6 is not available, use vnode.state.

+
+

Avoid anti-patterns

+

Although Mithril is flexible, some code patterns are discouraged:

+

Avoid fat components

+

Generally speaking, a "fat" component is a component that has custom instance methods. In other words, you should avoid attaching functions to vnode.state or this. It's exceedingly rare to have logic that logically fits in a component instance method and that can't be reused by other components. It's relatively common that said logic might be needed by a different component down the road.

+

It's easier to refactor code if that logic is placed in the data layer than if it's tied to a component state.

+

Consider this fat component:

+
// views/Login.js
+// AVOID
+var Login = {
+    username: "",
+    password: "",
+    setUsername: function(value) {
+        this.username = value
+    },
+    setPassword: function(value) {
+        this.password = value
+    },
+    canSubmit: function() {
+        return this.username !== "" && this.password !== ""
+    },
+    login: function() {/*...*/},
+    view: function() {
+        return m(".login", [
+            m("input[type=text]", {oninput: m.withAttr("value", this.setUsername.bind(this)), value: this.username}),
+            m("input[type=password]", {oninput: m.withAttr("value", this.setPassword.bind(this)), value: this.password}),
+            m("button", {disabled: !this.canSubmit(), onclick: this.login}, "Login"),
+        ])
+    }
+}
+
+

Normally, in the context of a larger application, a login component like the one above exists alongside components for user registration and password recovery. Imagine that we want to be able to prepopulate the email field when navigating from the login screen to the registration or password recovery screens (or vice versa), so that the user doesn't need to re-type their email if they happened to fill the wrong page (or maybe you want to bump the user to the registration form if a username is not found).

+

Right away, we see that sharing the username and password fields from this component to another is difficult. This is because the fat component encapsulates its our state, which by definition makes this state difficult to access from outside.

+

It makes more sense to refactor this component and pull the state code out of the component and into the application's data layer. This can be as simple as creating a new module:

+
// models/Auth.js
+// PREFER
+var Auth = {
+    username: "",
+    password: "",
+    setUsername: function(value) {
+        Auth.username = value
+    },
+    setPassword: function(value) {
+        Auth.password = value
+    },
+    canSubmit: function() {
+        return Auth.username !== "" && Auth.password !== ""
+    },
+    login: function() {/*...*/},
+}
+
+module.exports = Auth
+
+

Then, we can clean up the component:

+
// views/Login.js
+// PREFER
+var Auth = require("../models/Auth")
+
+var Login = {
+    view: function() {
+        return m(".login", [
+            m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
+            m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
+            m("button", {disabled: !Auth.canSubmit(), onclick: Auth.login}, "Login"),
+        ])
+    }
+}
+
+

This way, the Auth module is now the source of truth for auth-related state, and a Register component can easily access this data, and even reuse methods like canSubmit, if needed. In addition, if validation code is required (for example, for the email field), you only need to modify setEmail, and that change will do email validation for any component that modifies an email field.

+

As a bonus, notice that we no longer need to use .bind to keep a reference to the state for the component's event handlers.

+

Avoid restrictive interfaces

+

Try to keep component interfaces generic - using attrs and children directly - unless the component requires special logic to operate on input.

+

In the example below, the button configuration is severely limited: it does not support any events other than onclick, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML).

+
// AVOID
+var RestrictiveComponent = {
+    view: function(vnode) {
+        return m("button", {onclick: vnode.attrs.onclick}, [
+            "Click to " + vnode.attrs.text
+        ])
+    }
+}
+
+

If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node.

+
// PREFER
+var FlexibleComponent = {
+    view: function(vnode) {
+        return m("button", vnode.attrs, [
+            "Click to ", vnode.children
+        ])
+    }
+}
+
+

Don't manipulate children

+

If a component is opinionated in how it applies attributes or children, you should switch to using custom attributes.

+

Often it's desirable to define multiple sets of children, for example, if a component has a configurable title and body.

+

Avoid destructuring the children property for this purpose.

+
// AVOID
+var Header = {
+    view: function(vnode) {
+        return m(".section", [
+            m(".header", vnode.children[0]),
+            m(".tagline", vnode.children[1]),
+        ])
+    }
+}
+
+m(Header, [
+    m("h1", "My title"),
+    m("h2", "Lorem ipsum"),
+])
+
+// awkward consumption use case
+m(Header, [
+    [
+        m("h1", "My title"),
+        m("small", "A small note"),
+    ],
+    m("h2", "Lorem ipsum"),
+])
+
+

The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve children for uniform child content:

+
// PREFER
+var BetterHeader = {
+    view: function(vnode) {
+        return m(".section", [
+            m(".header", vnode.attrs.title),
+            m(".tagline", vnode.attrs.tagline),
+        ])
+    }
+}
+
+m(BetterHeader, {
+    title: m("h1", "My title"),
+    tagline: m("h2", "Lorem ipsum"),
+})
+
+// clearer consumption use case
+m(BetterHeader, {
+    title: [
+        m("h1", "My title"),
+        m("small", "A small note"),
+    ],
+    tagline: m("h2", "Lorem ipsum"),
+})
+
+

Define components statically, call them dynamically

+
Avoid creating component definitions inside views
+

If you create a component from within a view method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch.

+

For that reason you should avoid recreating components. Instead, consume components idiomatically.

+
// AVOID
+var ComponentFactory = function(greeting) {
+    // creates a new component on every call
+    return {
+        view: function() {
+            return m("div", greeting)
+        }
+    }
+}
+m.render(document.body, m(ComponentFactory("hello")))
+// calling a second time recreates div from scratch rather than doing nothing
+m.render(document.body, m(ComponentFactory("hello")))
+
+// PREFER
+var Component = {
+    view: function(vnode) {
+        return m("div", vnode.attrs.greeting)
+    }
+}
+m.render(document.body, m(Component, {greeting: "hello"}))
+// calling a second time does not modify DOM
+m.render(document.body, m(Component, {greeting: "hello"}))
+
+
Avoid creating component instances outside views
+

Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views:

+
// AVOID
+var Counter = {
+    count: 0,
+    view: function(vnode) {
+        return m("div",
+            m("p", "Count: " + vnode.state.count ),
+
+            m("button", {
+                onclick: function() {
+                    vnode.state.count++
+                }
+            }, "Increase count")
+        )
+    }
+}
+
+var counter = m(Counter)
+
+m.mount(document.body, {
+    view: function(vnode) {
+        return [
+            m("h1", "My app"),
+            counter
+        ]
+    }
+})
+
+

In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created:

+
// PREFER
+var Counter = {
+    count: 0,
+    view: function(vnode) {
+        return m("div",
+            m("p", "Count: " + vnode.state.count ),
+
+            m("button", {
+                onclick: function() {
+                    vnode.state.count++
+                }
+            }, "Increase count")
+        )
+    }
+}
+
+m.mount(document.body, {
+    view: function(vnode) {
+        return [
+            m("h1", "My app"),
+            m(Counter)
+        ]
+    }
+})
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Contributing

+ +

FAQ

+

How do I go about contributing ideas or new features?

+

Create an issue thread on Github to suggest your idea so the community can discuss it.

+

If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities.

+

How should I report bugs?

+

Ideally, the best way to report bugs is to provide a small snippet of code where the issue can be reproduced (via jsfiddle, jsbin, a gist, etc). Even better would be to submit a pull request with a fix and tests. If you don't know how to test your fix, or lint or whatever, submit anyways, and we can help you.

+

How do I send a pull request?

+

To send a pull request:

+
    +
  • fork the repo (button at the top right in Github)
  • +
  • clone the forked repo to your computer (green button in Github)
  • +
  • create a feature branch (run git checkout -b the-feature-branch-name)
  • +
  • make your changes
  • +
  • run the tests (run npm t)
  • +
  • submit a pull request (go to the pull requests tab in Github, click the green button and select your feature branch)
  • +
+

I'm submitting a PR. How do I run tests?

+

Assuming you have forked this repo, you can open the index.html file in a module's tests folder and look at console output to see only tests for that module, or you can run ospec/bin/ospec from the command line to run all tests.

+

While testing, you can modify a test to use o.only(description, test) instead of o(description, test) if you wish to run only a specific test to speed up your debugging experience. Don't forget to remove the .only after you're done!

+

There is no need to npm install anything in order to run the test suite, however NodeJS is required to run the test suite from the command line. You do need to npm install if you want to lint or get a code coverage report though.

+

How do I build Mithril?

+

If all you're trying to do is run examples in the codebase, you don't need to build Mithril, you can just open the various html files and things should just work.

+

To generate the bundled file for testing, run npm run dev from the command line. To generate the minified file, run npm run build. There is no need to npm install anything, but NodeJS is required to run the build scripts.

+

Is there a style guide?

+

Yes, there's an eslint configuration, but it's not strict about formatting at all. If your contribution passes npm run lint, it's good enough for a PR (and it can still be accepted even if it doesn't pass).

+

Spacing and formatting inconsistencies may be fixed after the fact, and we don't want that kind of stuff getting in the way of contributing.

+

Why do tests mock the browser APIs?

+

Most notoriously, because it's impossible to test the router and some side effects properly otherwise. Also, mocks allow the tests to run under Node.js without requiring heavy dependencies like PhantomJS/ChromeDriver/JSDOM.

+

Another important reason is that it allows us to document browser API quirks via code, through the tests for the mocks.

+

Why does Mithril use its own testing framework and not Mocha/Jasmine/Tape?

+

Mainly to avoid requiring dependencies. ospec is customized to provide only essential information for common testing workflows (namely, no spamming ok's on pass, and accurate noiseless errors on failure)

+

Why do tests use module/module.js? Why not use Browserify, Webpack or Rollup?

+

Again, to avoid requiring dependencies. The Mithril codebase is written using a statically analyzable subset of CommonJS module definitions (as opposed to ES6 modules) because its syntax is backwards compatible with ES5, therefore making it possible to run source code unmodified in browsers without the need for a build tool or a file watcher.

+

This simplifies the workflow for bug fixes, which means they can be fixed faster.

+

Why doesn't the Mithril codebase use ES6 via Babel? Would a PR to upgrade be welcome?

+

Being able to run Mithril raw source code in IE is a requirement for all browser-related modules in this repo.

+

In addition, ES6 features are usually less performant than equivalent ES5 code, and transpiled code is bulkier.

+

Why doesn't the Mithril codebase use trailing semi-colons? Would a PR to add them be welcome?

+

I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent.

+

Why does the Mithril codebase use a mix of instanceof and typeof checks instead of Object.prototype.toString.call, Array.isArray, etc? Would a PR to refactor those checks be welcome?

+

Mithril avoids peeking at objects' [[class]] string for performance considerations. Many type checks are seemingly inconsistent, weird or convoluted because those specific constructs demonstrated the best performance profile in benchmarks compared to alternatives.

+

Type checks are generally already irreducible expressions and having micro-modules for type checking subroutines would add maintenance overhead.

+ +

You should be trying to reduce the number of DOM operations or reduce algorithmic complexity in a hot spot. Anything else is likely a waste of time. Specifically, micro-optimizations like caching array lengths, caching object property values and inlining functions won't have any positive impact in modern javascript engines.

+

Keep object properties consistent (i.e. ensure the data objects always have the same properties and that properties are always in the same order) to allow the engine to keep using JIT'ed structs instead of hashmaps. Always place null checks first in compound type checking expressions to allow the Javascript engine to optimize to type-specific code paths. Prefer for loops over Array methods and try to pull conditionals out of loops if possible.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Credits

+ +

Mithril was originally written by Leo Horie, but it is where it is today thanks to the hard work and great ideas of many people.

+

Special thanks to:

+
    +
  • Pat Cavit, who exposed most of the public API for Mithril 1.0, brought in test coverage and automated the publishing process
  • +
  • Isiah Meadows, who brought in linting, modernized the test suite and has been a strong voice in design discussions
  • +
  • Zoli Kahan, who replaced the original Promise implementation with one that actually worked properly
  • +
  • Alec Embke, who single-handedly wrote the JSON-P implementation
  • +
  • Barney Carroll, who suggested many great ideas and relentlessly pushed Mithril to the limit to uncover design issues prior to Mithril 1.0
  • +
  • Dominic Gannaway, who offered insanely meticulous technical insight into rendering performance
  • +
  • Boris Letocha, whose search space reduction algorithm is the basis for Mithril's virtual DOM engine
  • +
  • Joel Richard, whose monomorphic virtual DOM structure is the basis for Mithril's vnode implementation
  • +
  • Simon Friis Vindum, whose open source work was an inspiration to many design decisions for Mithril 1.0
  • +
  • Boris Kaul, for his awesome work on the benchmarking tools used to develop Mithril
  • +
  • Leon Sorokin, for writing a DOM instrumentation tool that helped improve performance in Mithril 1.0
  • +
  • Jordan Walke, whose work on React was prior art to the implementation of keys in Mithril
  • +
  • Pierre-Yves Gérardy, who consistently makes high quality contributions
  • +
+

Other people who also deserve recognition:

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

CSS

+ +
+

Vanilla CSS

+

For various reasons, CSS has a bad reputation and often developers reach for complex tools in an attempt to make styling more manageable. In this section, we'll take a step back and cover some tips on writing plain CSS:

+
    +
  • Avoid using the space operator - The vast majority of CSS maintainability issues are due to CSS specificity issues. The space operator defines a descendant (e.g. .a .b) and at the same time, it increases the level of specificity for the CSS rules that apply to that selector, sometimes overriding styles unexpectedly.

    +

    Instead, it's preferable to share a namespace prefix in all class names that belong to a logical group of elements:

    +
      /* AVOID */
    +  .chat.container {/*...*/}
    +  .chat .item {/*...*/}
    +  .chat .avatar {/*...*/}
    +  .chat .text {/*...*/}
    +
    +  /* PREFER */
    +  .chat-container {/*...*/}
    +  .chat-item {/*...*/}
    +  .chat-avatar {/*...*/}
    +  .chat-text {/*...*/}
    +
    +
  • +
  • Use only single-class selectors - This convention goes hand-in-hand with the previous one: avoiding high specificity selectors such as #foo or div.bar help decrease the likelyhood of specificity conflicts.

    +
      /* AVOID */
    +  #home {}
    +  input.highlighted {}
    +
    +  /* PREFER */
    +  .home {}
    +  .input-highlighted {}
    +
    +
  • +
  • Develop naming conventions - You can reduce naming collisions by defining keywords for certain types of UI elements. This is particularly effective when brand names are involved:

    +
      /* AVOID */
    +  .twitter {} /* icon link in footer */
    +  .facebook {} /* icon link in footer */
    +  /* later... */
    +  .modal.twitter {} /* tweet modal */
    +  .modal.facebook {} /* share modal */
    +
    +  /* PREFER */
    +  .link-twitter {}
    +  .link-facebook {}
    +  /* later... */
    +  .modal-twitter {}
    +  .modal-facebook {}
    +
    +
  • +
+
+

Tachyons

+

Tachyons is a CSS framework, but the concept behind it can easily be used without the library itself.

+

The basic idea is that every class name must declare one and only one CSS rule. For example, bw1 stands for border-width:1px;. To create a complex style, one simply combines class names representing each of the required CSS rules. For example, .black.bg-dark-blue.br2 styles an element with blue background, black text and a 4px border-radius.

+

Since each class is small and atomic, it's essentially impossible to run into CSS conflicts.

+

As it turns out, the Tachyons convention fits extremely well with Mithril and JSX:

+
var Hero = ".black.bg-dark-blue.br2.pa3"
+
+m.mount(document.body, <Hero>Hello</Hero>)
+// equivalent to `m(".black.bg-dark.br2.pa3", "Hello")`
+
+
+

CSS in JS

+

In plain CSS, all selectors live in the global scope and a prone to name collisions and specificity conflicts. CSS-in-JS aims to solve the issue of scoping in CSS, i.e. it groups related styles into non-global modules that are invisible to each other. CSS-in-JS is suitable for extremely large dev teams working on a single codebase, but it's not a good choice for most teams.

+

Nowadays there are a lot of CSS-in-JS libraries with various degrees of robustness.

+

The main problem with many of these libraries is that even though they require a non-trivial amount of transpiler tooling and configuration, they also require sacrificing code readability in order to work, e.g. <a class={classnames(styles.button, styles.danger)}></a> vs <a class="button danger"></a> (or m("a.button.danger") if we're using hyperscript).

+

Often sacrifices also need to be made at time of debugging, when mapping rendered CSS class names back to their source. Often all you get in browser developer tools is a class like button_fvp6zc2gdj35evhsl73ffzq_0 danger_fgdl0s2a5fmle5g56rbuax71_0 with useless source maps (or worse, entirely criptic class names).

+

Another common issue is lack of support for less basic CSS features such as @keyframes and @font-face.

+

If you are adamant on using a CSS-in-JS library, consider using J2C.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

ES6

+ +
+

Mithril is written in ES5, and is fully compatible with ES6 as well. ES6 is a recent update to Javascript that introduces new syntax sugar for various common cases. It's not yet fully supported by all major browsers and it's not a requirement for writing application, but it may be pleasing to use depending on your team's preferences.

+

In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like Babel is required to compile ES6 features down to ES5.

+

Setup

+

The simplest way to setup an ES6 compilation toolchain is via Babel.

+

Babel requires NPM, which is automatically installed when you install Node.js. Once NPM is installed, create a project folder and run this command:

+
npm init -y
+
+

If you want to use Webpack and Babel together, skip to the section below.

+

To install Babel as a standalone tool, use this command:

+
npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev
+
+

Create a .babelrc file:

+
{
+    "presets": ["es2015"],
+    "plugins": [
+        ["transform-react-jsx", {
+            "pragma": "m"
+        }]
+    ]
+}
+
+

To run Babel as a standalone tool, run this from the command line:

+
babel src --out-dir bin --source-maps
+
+

Using Babel with Webpack

+

If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps.

+
npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev
+
+

Create a .babelrc file:

+
{
+    "presets": ["es2015"],
+    "plugins": [
+        ["transform-react-jsx", {
+            "pragma": "m"
+        }]
+    ]
+}
+
+

Next, create a file called webpack.config.js

+
module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: './bin',
+        filename: 'app.js',
+    },
+    module: {
+        loaders: [{
+            test: /\.js$/,
+            exclude: /node_modules/,
+            loader: 'babel-loader'
+        }]
+    }
+}
+
+

This configuration assumes the source code file for the application entry point is in src/index.js, and this will output the bundle to bin/app.js.

+

To run the bundler, setup an npm script. Open package.json and add this entry under "scripts":

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch"
+    }
+}
+
+

You can now then run the bundler by running this from the command line:

+
npm start
+
+

Production build

+

To generate a minified file, open package.json and add a new npm script called build:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch",
+        "build": "webpack -p"
+    }
+}
+
+

You can use hooks in your production environment to run the production build script automatically. Here's an example for Heroku:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch",
+        "build": "webpack -p",
+        "heroku-postbuild": "webpack -p"
+    }
+}
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Examples

+ +

Here are some examples of Mithril in action

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

fragment(attrs, children)

+ +
+

Description

+

Allows attaching lifecycle methods to a fragment vnode

+
var groupVisible = true
+var log = function() {
+    console.log("group is now visible")
+}
+
+m("ul", [
+    m("li", "child 1"),
+    m("li", "child 2"),
+    groupVisible ? m.fragment({oninit: log}, [
+        // a fragment containing two elements
+        m("li", "child 3"),
+        m("li", "child 4"),
+    ]) : null
+])
+
+
+

Signature

+

Generates a fragment vnode

+

vnode = m.fragment(attrs, children)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
attrsObjectYesA map of attributes
childrenArray<Vnode>|String|Number|BooleanYesA list of vnodes
returnsVnodeA fragment vnode
+

How to read signatures

+
+

How it works

+

m.fragment() creates a fragment vnode with attributes. It is meant for advanced use cases involving keys or lifecyle methods.

+

A fragment vnode represents a list of DOM elements. If you want a regular element vnode that represents only one DOM element, you should use m() instead.

+

Normally you can use simple arrays instead to denote a list of nodes:

+
var groupVisible = true
+
+m("ul", [
+    m("li", "child 1"),
+    m("li", "child 2"),
+    groupVisible ? [
+        // a fragment containing two elements
+        m("li", "child 3"),
+        m("li", "child 4"),
+    ] : null
+])
+
+

However, Javascript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead.

+

There are a few benefits that come from using m.fragment instead of handwriting a vnode object structure: m.fragment creates monomorphic objects, which have better performance characteristics than creating objects dynamically. In addition, using m.fragment makes your intentions clear to other developers, and it makes it less likely that you'll mistakenly set attributes on the vnode object itself rather than on its attrs map.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Framework comparison

+ +

If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril would help you solve your problems more effectively.

+
+

Why not [insert favorite framework here]?

+

The reality is that most modern frameworks are fast, well-suited to build complex applications, and maintainable if you know how to use them effectively. There are examples of highly complex applications in the wild using just about every popular framework: Udemy uses Angular, AirBnB uses React, Gitlab uses Vue, Guild Wars 2 uses Mithril (yes, inside the game!). Clearly, these are all production-quality frameworks.

+

As a rule of thumb, if your team is already heavily invested in another framework/library/stack, it makes more sense to stick with it, unless your team agrees that there's a very strong reason to justify a costly rewrite.

+

However, if you're starting something new, do consider giving Mithril a try, if nothing else, to see how much value Mithril adopters have been getting out of 8kb (gzipped) of code. Mithril is used by many well-known companies (e.g. Vimeo, Nike, Fitbit), and it powers large open-sourced platforms too (e.g. Lichess, Flarum).

+
+

Why use Mithril?

+

In one sentence: because Mithril is pragmatic. The 10 minutes guide is a good example: that's how long it takes to learn components, XHR and routing - and that's just about the right amount of knowledge needed to build useful applications.

+

Mithril is all about getting meaningful work done efficiently. Doing file uploads? The docs show you how. Authentication? Documented too. Exit animations? You got it. No extra libraries, no magic.

+
+

Comparisons

+

React

+

React is a view library maintained by Facebook.

+

React and Mithril share a lot of similarities. If you already learned React, you already know almost all you need to build apps with Mithril.

+
    +
  • They both use virtual DOM, lifecycle methods and key-based reconciliation
  • +
  • They both organize views via components
  • +
  • They both use Javascript as a flow control mechanism within views
  • +
+

The most obvious difference between React and Mithril is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that React-based architectures can vary wildly from project to project, and that those projects are that much more likely to cross the 1MB size line.

+

Mithril has built-in modules for common necessities such as routing and XHR, and the guide demonstrates idiomatic usage. This approach is preferable for teams that value consistency and ease of onboarding.

+

Performance

+

Both React and Mithril care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using innerHTML). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's shouldComponentUpdate hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions.

+

Mithril follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run.

+

Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a console.time() call on the first line and a console.timeEnd() call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:

+ + + + + + + + + + + + + +
ReactMithril
55.8 ms4.5 ms
+

Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.

+

Since this is a micro-benchmark, you are encourage to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls console.time and console.timeEnd to the bundled script. Avoid using new Date and performance.now, as those mechanisms are not as statistically accurate.

+

For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the benchmark for React is here, and the benchmark for Mithril is here. Note that we're benchmarking all of Mithril rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril's initialization speed is noticeably better than React.

+

Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for React and Mithril. Their best results are shown below:

+ + + + + + + + + + + + + +
ReactMithril
99.7 ms42.8 ms
+

What these numbers show is that not only does Mithril initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use.

+
Update performance
+

Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running.

+

A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and setTimeout clamping delay, so the most meaningful number to look at is the mean render time. You can compare a React implementation and a Mithril implementation. Sample results are shown below:

+ + + + + + + + + + + + + +
ReactMithril
12.1 ms6.4 ms
+
Development performance
+

Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, here's the 10,000 node benchmark from above using the development version of React.

+
Drop-in replacements
+

There are several projects that claim API parity with React (some via compatibility layer libraries), but they are not fully compatible (e.g. PropType support is usually stubbed out, synthetic events are sometimes not supported, and some APIs have different semantics). Note that these libraries typically also include features of their own that are not part of the official React API, which may become problematic down the road if one decides to switch back to React Fiber.

+

Claims about small download size (compared to React) are accurate, but most of these libraries are slightly larger than Mithril's renderer module. Preact is the only exception.

+

Be wary of aggressive performance claims, as benchmarks used by some of these projects are known to be out-of-date and flawed (in the sense that they can be - and are - exploited). Boris Kaul (author of some of the benchmarks) has written in detail about how benchmarks are gamed. Another thing to keep in mind is that some benchmarks aggressively use advanced optimization features and thus demonstrate potential performance, i.e. performance that is possible given some caveats, but realistically unlikely unless you actively spend the time to go over your entire codebase identifying optimization candidates and evaluating the regression risks brought by the optimization caveats.

+

In the spirit of demonstrating typical performance characteristics, the benchmarks presented in this comparison page are implemented in an apples-to-apples, naive, idiomatic way (i.e. the way you would normally write 99% of your code) and do not employ tricks or advanced optimizations to make one or other framework look artificially better. You are encouraged to contribute a PR if you feel any DbMonster implementation here could be written more idiomatically.

+

Complexity

+

Both React and Mithril have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading).

+

While complex toolchains are also possible with Mithril and other frameworks alike, it's strongly recommended that you follow the KISS and YAGNI principles when using Mithril.

+

Learning curve

+

Both React and Mithril have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril components is nearly identical. There are obviously more APIs to learn in Mithril, since Mithril also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios.

+

Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel.

+

Documentation

+

React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts. Unfortunately, since React is limited to being only a view library, its documentation does not explore how to use React idiomatically in the context of a real-life application. As a result, there are many popular state management libraries and thus architectures using React can differ drastically from company to company (or even between projects).

+

Mithril documentation also includes introductory tutorials, pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.

+

Mithril documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.

+
+

Angular

+

Angular is a web application framework maintained by Google.

+

Angular and Mithril are fairly different, but they share a few similarities:

+
    +
  • Both support componentization
  • +
  • Both have an array of tools for various aspects of web applications (e.g. routing, XHR)
  • +
+

The most obvious difference between Angular and Mithril is in their complexity. This can be seen most easily in how views are implemented. Mithril views are plain Javascript, and flow control is done with Javascript built-in mechanisms such as ternary operators or Array.prototype.map. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate Javascript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in Javascript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates Javascript functions dynamically for performance, and a slower mode for dealing with Content Security Policy restrictions).

+

Performance

+

Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large $scope structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril is often faster than Angular, due to the ease of auditing that Mithril's small codebase size affords.

+

It's difficult to make a comparison of load times between Angular and Mithril for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application.

+

With that being said, the smallest known Angular 2 bundle is a 29kb hello world compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril hello world - including the entire Mithril core - would not be over 8kb gzipped (a more optimized bundle could easily be half of that).

+

Also, remember that frameworks like Angular and Mithril are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb.

+
Update performance
+

A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and setTimeout clamping delay, so the most meaningful number to look at is the mean render time. You can compare an Angular implementation and a Mithril implementation. Both implementations are naive (i.e. no optimizations). Sample results are shown below:

+ + + + + + + + + + + + + +
AngularMithril
11.5 ms6.4 ms
+

Complexity

+

Angular is superior to Mithril in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare Angular's API surface with Mithril's. You can make your own judgment on which API is more self-descriptive and more relevant to your needs.

+

Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what.

+

Learning curve

+

If we compare apples to apples, Angular 2 and Mithril have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools.

+

With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what Javascript does natively in Mithril - Angular's ng-if/ngIf is a directive, which uses a custom parser and compiler to evaluate an expression string and emulate lexical scoping... and so on. Mithril tends to be a lot more transparent, and therefore easier to reason about.

+

Documentation

+

Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for.

+

Mithril documentation includes introductory tutorials, pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference.

+

Mithril documentation also demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.

+
+

Vue

+

Vue is a view library similar to Angular.

+

Vue and Mithril have a lot of differences but they also share some similarities:

+
    +
  • They both use virtual DOM and lifecycle methods
  • +
  • Both organize views via components
  • +
+

Vue 2 uses a fork of Snabbdom as its virtual DOM system. In addition, Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive system that overwrites native methods in a component's data tree (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril or React views, rather than Angular's compiled rendering functions.

+

Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril is around 4kb gzipped). Both have similar performance characteristics, but benchmarks usually suggest Mithril is slightly faster.

+

Performance

+

Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a console.time() call on the first line and a console.timeEnd() call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop:

+ + + + + + + + + + + + + +
VueMithril
21.8 ms4.5 ms
+

Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques.

+
Update performance
+

A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and setTimeout clamping delay, so the most meaningful number to look at is the mean render time. You can compare an Angular implementation and a Mithril implementation. Both implementations are naive (i.e. no optimizations). Sample results are shown below:

+ + + + + + + + + + + + + +
VueMithril
9.8 ms6.4 ms
+

Complexity

+

Vue is heavily inspired by Angular and has many things that Angular does (e.g. directives, filters, bi-directional bindings, v-cloak), but also has things inspired by React (e.g. components). As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins). Vue provides both bi-directional data binding and an optional Redux-like state management library, but unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach can cause architectural fragmentation in long-lived projects.

+

Mithril has far less concepts and typically organizes applications in terms of components and a data layer. There are no different ways of defining components, and thus there's no need to install different sets of tools to make different flavors work.

+

Documentation

+

Both Vue and Mithril have good documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts.

+

However, due to Vue's many-ways-to-do-one-thing approach, some things may not be adequately documented. For example, there's no documentation on hyperscript syntax or usage.

+

Mithril documentation typically errs on the side of being overly thorough if a topic involves things outside of the scope of Mithril. For example, when a topic involves a 3rd party library, Mithril documentation walks through the installation process for the 3rd party library. Mithril documentation also often demonstrates simple, close-to-the-metal solutions to common use cases in real-life applications where it's appropriate to inform a developer that web standards may be now on par with larger established libraries.

+

Mithril's tutorials also cover a lot more ground than Vue's: the Vue tutorial finishes with a static list of foodstuff. Mithril's 10 minute guide covers the majority of its API and goes over key aspects of real-life applications, such as fetching data from a server and routing (and there's a longer, more thorough tutorial if that's not enough).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

m(selector, attributes, children)

+ +
+

Description

+

Represents an HTML element in a Mithril view

+
m("div", {class: "foo"}, "hello")
+// represents <div class="foo">hello</div>
+
+

You can also use HTML syntax via a Babel plugin.

+
/** jsx m */
+<div class="foo">hello</div>
+
+
+

Signature

+

vnode = m(selector, attributes, children)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
selectorString|ObjectYesA CSS selector or a component
attributesObjectNoHTML attributes or element properties
childrenArray<Vnode>|String|Number|BooleanNoChild vnodes. Can be written as splat arguments
returnsVnodeA vnode
+

How to read signatures

+
+

How it works

+

Mithril provides a hyperscript function m(), which allows expressing any HTML structure using javascript syntax. It accepts a selector string (required), an attributes object (optional) and a children array (optional).

+
m("div", {id: "box"}, "hello")
+
+// equivalent HTML:
+// <div id="box">hello</div>
+
+

The m() function does not actually return a DOM element. Instead it returns a virtual DOM node, or vnode, which is a javascript object that represents the DOM element to be created.

+
// a vnode
+var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}
+
+

To transform a vnode into an actual DOM element, use the m.render() function:

+
m.render(document.body, m("br")) // puts a <br> in <body>
+
+

Calling m.render() multiple times does not recreate the DOM tree from scratch each time. Instead, each call will only make a change to a DOM tree if it is absolutely necessary to reflect the virtual DOM tree passed into the call. This behavior is desirable because recreating the DOM from scratch is very expensive, and causes issues such as loss of input focus, among other things. By contrast, updating the DOM only where necessary is comparatively much faster and makes it easier to maintain complex UIs that handle multiple user stories.

+
+

Flexibility

+

The m() function is both polymorphic and variadic. In other words, it's very flexible in what it expects as input parameters:

+
// simple tag
+m("div") // <div></div>
+
+// attributes and children are optional
+m("a", {id: "b"}) // <a id="b"></a>
+m("span", "hello") // <span>hello</span>
+
+// tag with child nodes
+m("ul", [             // <ul>
+    m("li", "hello"), //   <li>hello</li>
+    m("li", "world"), //   <li>world</li>
+])                    // </ul>
+
+// array is optional
+m("ul",               // <ul>
+    m("li", "hello"), //   <li>hello</li>
+    m("li", "world")  //   <li>world</li>
+)                     // </ul>
+
+
+

CSS selectors

+

The first argument of m() can be any CSS selector that can describe an HTML element. It accepts any valid CSS combinations of # (id), . (class) and [] (attribute) syntax.

+
m("div#hello")
+// <div id="hello"></div>
+
+m("section.container")
+// <section class="container"></section>
+
+m("input[type=text][placeholder=Name]")
+// <input type="text" placeholder="Name" />
+
+m("a#exit.external[href='http://example.com']", "Leave")
+// <a id="exit" class="external" href="http://example.com">Leave</a>
+
+

If you omit the tag name, Mithril assumes a div tag.

+
m(".box.box-bordered") // <div class="box box-bordered"></div>
+
+

Typically, it's recommended that you use CSS selectors for static attributes (i.e. attributes whose value do not change), and pass an attributes object for dynamic attribute values.

+
var currentURL = "/"
+
+m("a.link[href=/]", {
+    class: currentURL === "/" ? "selected" : ""
+}, "Home")
+
+// equivalent HTML:
+// <a href="/" class="link selected">Home</a>
+
+

If there are class names in both first and second arguments of m(), they are merged together as you would expect.

+
+

DOM attributes

+

Mithril uses both the Javascript API and the DOM API (setAttribute) to resolve attributes. This means you can use both syntaxes to refer to attributes.

+

For example, in the Javascript API, the readonly attribute is called element.readOnly (notice the uppercase). In Mithril, all of the following are supported:

+
m("input", {readonly: true}) // lowercase
+m("input", {readOnly: true}) // uppercase
+m("input[readonly]")
+m("input[readOnly]")
+
+
+

Style attribute

+

Mithril supports both strings and objects as valid style values. In other words, all of the following are supported:

+
m("div", {style: "background:red;"})
+m("div", {style: {background: "red"}})
+m("div[style=background:red]")
+
+

Using a string as a style would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.

+

Mithril does not attempt to add units to number values.

+
+

Events

+

Mithril supports event handler binding for all DOM events, including events whose specs do not define an on${event} property, such as touchstart

+
function doSomething(e) {
+    console.log(e)
+}
+
+m("div", {onclick: doSomething})
+
+
+

Properties

+

Mithril supports DOM functionality that is accessible via properties such as <select>'s selectedIndex and value properties.

+
m("select", {selectedIndex: 0}, [
+    m("option", "Option A"),
+    m("option", "Option B"),
+])
+
+
+

Components

+

Components allow you to encapsulate logic into a unit and use it as if it was an element. They are the base for making large, scalable applications.

+

A component is any Javascript object that contains a view method. To consume a component, pass the component as the first argument to m() instead of passing a CSS selector string. You can pass arguments to the component by defining attributes and children, as shown in the example below.

+
// define a component
+var Greeter = {
+    view: function(vnode) {
+        return m("div", vnode.attrs, ["Hello ", vnode.children])
+    }
+}
+
+// consume it
+m(Greeter, {style: "color:red;"}, "world")
+
+// equivalent HTML:
+// <div style="color:red;">Hello world</div>
+
+

To learn more about components, see the components page.

+
+

Lifecycle methods

+

Vnodes and components can have lifecycle methods (also known as hooks), which are called at various points during the lifetime of a DOM element. The lifecycle methods supported by Mithril are: oninit, oncreate, onupdate, onbeforeremove, onremove, and onbeforeupdate.

+

Lifecycle methods are defined in the same way as DOM event handlers, but receive the vnode as an argument, instead of an Event object:

+
function initialize(vnode) {
+    console.log(vnode)
+}
+
+m("div", {oninit: initialize})
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HookDescription
oninit(vnode)Runs before a vnode is rendered into a real DOM element
oncreate(vnode)Runs after a vnode is appended to the DOM
onupdate(vnode)Runs every time a redraw occurs while the DOM element is attached to the document
onbeforeremove(vnode)Runs before a DOM element is removed from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements.
onremove(vnode)Runs before a DOM element is removed from the document. If a onbeforeremove hook is defined, onremove is called after done is called. This method is triggered on the element that is detached from its parent element, and all of its children
onbeforeupdate(vnode, old)Runs before onupdate and if it returns false, it prevents a diff for the element and all of its children
+

To learn more about lifecycle methods, see the lifecycle methods page.

+
+

Keys

+

Vnodes in a list can have a special attribute called key, which can be used to manage the identity of the DOM element as the model data that generates the vnode list changes.

+

Typically, key should be the unique identifier field of the objects in the data array.

+
var users = [
+    {id: 1, name: "John"},
+    {id: 2, name: "Mary"},
+]
+
+function userInputs(users) {
+    return users.map(function(u) {
+        return m("input", {key: u.id}, u.name)
+    })
+}
+
+m.render(document.body, userInputs(users))
+
+

Having a key means that if the users array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.

+

To learn more about keys, see the keys page

+
+

SVG and MathML

+

Mithril fully supports SVG. Xlink is also supported, but unlike in pre-v1.0 versions of Mithril, must have the namespace explicitly defined:

+
m("svg", [
+    m("image[xlink:href='image.gif']")
+])
+
+

MathML is also fully supported.

+
+

Making templates dynamic

+

Since nested vnodes are just plain Javascript expressions, you can simply use Javascript facilities to manipulate them

+

Dynamic text

+
var user = {name: "John"}
+
+m(".name", user.name) // <div class="name">John</div>
+
+

Loops

+

Use Array methods such as map to iterate over lists of data

+
var users = [
+    {name: "John"},
+    {name: "Mary"},
+]
+
+m("ul", users.map(function(u) { // <ul>
+    return m("li", u.name)      //   <li>John</li>
+                                //   <li>Mary</li>
+}))                             // </ul>
+
+// ES6:
+// m("ul", users.map(u =>
+//   m("li", u.name)
+// ))
+
+

Conditionals

+

Use the ternary operator to conditionally set content on a view

+
var isError = false
+
+m("div", isError ? "An error occurred" : "Saved") // <div>Saved</div>
+
+

You cannot use Javascript statements such as if or for within Javascript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations.

+
+

Converting HTML

+

In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX.

+

When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can use the HTML-to-Mithril-template converter.

+
+

Avoid Anti-patterns

+

Although Mithril is flexible, some code patterns are discouraged:

+

Avoid dynamic selectors

+

Different DOM elements have different attributes, and often different behaviors. Making a selector configurable can leak the implementation details of a component out of its unit.

+
// AVOID
+var BadInput = {
+    view: function(vnode) {
+        return m("div", [
+            m("label"),
+            m(vnode.attrs.type || "input")
+        ])
+    }
+}
+
+

Instead of making selectors dynamic, you are encouraged to explicitly code each valid possibility, or refactor the variable portion of the code out.

+
// PREFER explicit code
+var BetterInput = {
+    view: function(vnode) {
+        return m("div", [
+            m("label", vnode.attrs.title),
+            m("input"),
+        ])
+    }
+}
+var BetterSelect = {
+    view: function(vnode) {
+        return m("div", [
+            m("label", vnode.attrs.title),
+            m("select"),
+        ])
+    }
+}
+
+// PREFER refactor variability out
+var BetterLabeledComponent = {
+    view: function(vnode) {
+        return m("div", [
+            m("label", vnode.attrs.title),
+            vnode.children,
+        ])
+    }
+}
+
+

Avoid statements in view methods

+

Javascript statements often require changing the naturally nested structure of an HTML tree, making the code more verbose and harder to understand. Constructing an virtual DOM tree procedurally can also potentially trigger expensive deoptimizations (such as an entire template being recreated from scratch)

+
// AVOID
+var BadListComponent = {
+    view: function(vnode) {
+        var list = []
+        for (var i = 0; i < vnode.attrs.items.length; i++) {
+            list.push(m("li", vnode.attrs.items[i]))
+        }
+
+        return m("ul", list)
+    }
+}
+
+

Instead, prefer using Javascript expressions such as the ternary operator and Array methods.

+
// PREFER
+var BetterListComponent = {
+    view: function() {
+        return m("ul", vnode.attrs.items.map(function(item) {
+            return m("li", item)
+        }))
+    }
+}
+
+

Avoid creating vnodes outside views

+

When a redraw encounters a vnode which is strictly equal to the one in the previous render, it will be skipped and its contents will not be updated. While this may seem like an opportunity for performance optimisation, it should be avoided because it prevents dynamic changes in that node's tree - this leads to side-effects such as downstream lifecycle methods failing to trigger on redraw. In this sense, Mithril vnodes are immutable: new vnodes are compared to old ones; mutations to vnodes are not persisted.

+

The component documentation contains more detail and an example of this anti-pattern.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Introduction

+ +
+

What is Mithril?

+

Mithril is a modern client-side Javascript framework for building Single Page Applications.
It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box.

+

+
+
+
Download size
+ Mithril (8kb) +
+ Vue + Vue-Router + Vuex + fetch (40kb) +
+ React + React-Router + Redux + fetch (64kb) +
+ Angular (135kb) +
+
+
+
Performance
+ Mithril (6.4ms) +
+ Vue (9.8ms) +
+ React (12.1ms) +
+ Angular (11.5ms) +
+
+
+ +

Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess.

+

If you are an experienced developer and want to know how Mithril compares to other frameworks, see the framework comparison page.

+
+

Getting started

+

The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes.

+

Let's create an HTML file to follow along:

+
<body></body>
+<script src="http://cdn.rawgit.com/lhorie/mithril.js/rewrite/mithril.js"></script>
+<script>
+var root = document.body
+
+// your code goes here!
+</script>
+
+
+

Hello world

+

Let's start as small as well can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better)

+
var root = document.body
+
+m.render(root, "Hello world")
+
+

Now, let's change the text to something else. Add this line of code under the previous one:

+
m.render(root, "My first app")
+
+

As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch.

+
+

DOM elements

+

Let's wrap our text in an <h1> tag.

+
m.render(root, m("h1", "My first app"))
+
+

The m() function can be used to describe any HTML structure you want. So if you to add a class to the <h1>:

+
m("h1", {class: "title"}, "My first app")
+
+

If you want to have multiple elements:

+
[
+    m("h1", {class: "title"}, "My first app"),
+    m("button", "A button"),
+]
+
+

And so on:

+
m("main", [
+    m("h1", {class: "title"}, "My first app"),
+    m("button", "A button"),
+])
+
+

Note: If you prefer <html> syntax, it's possible to use it via a Babel plugin.

+
// HTML syntax via Babel's JSX plugin
+<main>
+    <h1 class="title">My first app</h1>
+    <button>A button</button>
+</main>
+
+
+

Components

+

A Mithril component is just an object with a view function. Here's the code above as a component:

+
var Hello = {
+    view: function() {
+        return m("main", [
+            m("h1", {class: "title"}, "My first app"),
+            m("button", "A button"),
+        ])
+    }
+}
+
+

To activate the component, we use m.mount.

+
m.mount(root, Hello)
+
+

As you would expect, doing so creates this markup:

+
<main>
+    <h1 class="title">My first app</h1>
+    <button>A button</button>
+</main>
+
+

The m.mount function is similar to m.render, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events:

+
var count = 0 // added a variable
+
+var Hello = {
+    view: function() {
+        return m("main", [
+            m("h1", {class: "title"}, "My first app"),
+            // changed the next line
+            m("button", {onclick: function() {count++}}, count + " clicks"),
+        ])
+    }
+}
+
+m.mount(root, Hello)
+
+

We defined an onclick event on the button, which increments a variable count (which was declared at the top). We are now also rendering the value of that variable in the button label.

+

You can now update the label of the button by clicking the button. Since we used m.mount, you don't need to manually call m.render to apply the changes in the count variable to the HTML; Mithril does it for you.

+

If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates.

+
+

Routing

+

Routing just means going from one screen to another in an application with several screens.

+

Let's add a splash page that appears before our click counter. First we create a component for it:

+
var Splash = {
+    view: function() {
+        return m("a", {href: "#!/hello"}, "Enter!")
+    }
+}
+
+

As you can see, this component simply renders a link to #!/hello. The #! part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the /hello part) is a route path.

+

Now that we going to have more than one screen, we use m.route instead of m.mount.

+
m.route(root, "/splash", {
+    "/splash": Splash,
+    "/hello": Hello,
+})
+
+

The m.route function still has the same auto-redrawing functionality that m.mount does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a #! in the URL.

+

The "/splash" right after root means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (/splash and /hello, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is http://localhost, then you get redirected to http://localhost/#!/splash.

+

Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to http://localhost/#!/hello. You can navigate back and forth to the splash page using the browser's back and next button.

+
+

XHR

+

Basically, XHR is just a way to talk to a server.

+

Let's change our click counter to make it save data on a server. For the server, we'll use REM, a mock REST API designed for toy apps like this tutorial.

+

First we create a function that calls m.request. The url specifies an endpoint that represents a resource, the method specifies the type of action we're taking (typically the PUT method upserts), data is the payload that we're sending to the endpoint and useCredentials means to enable cookies (a requirement for the REM API to work)

+
var count = 0
+var increment = function() {
+    m.request({
+        method: "PUT",
+        url: "http://rem-rest-api.herokuapp.com/api/tutorial/1",
+        data: {count: count + 1},
+        useCredentials: true,
+    })
+    .then(function(data) {
+        count = parseInt(data.count)
+    })
+}
+
+

Calling the increment function upserts an object {count: 1} to the /api/tutorial/1 endpoint. This endpoint returns an object with the same count value that was sent to it. Notice that the count variable is only updated after the request completes, and it's updated with the response value from the server now.

+

Let's replace the event handler in the component to call the increment function instead of incrementing the count variable directly:

+
var Hello = {
+    view: function() {
+        return m("main", [
+            m("h1", {class: "title"}, "My first app"),
+            m("button", {onclick: increment}, count + " clicks"),
+        ])
+    }
+}
+
+

Clicking the button should now update the count.

+
+

We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR.

+

This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, be sure to check out the simple application tutorial, which walks you through building a realistic application.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Installation

+ +

CDN

+

If you're new to Javascript or just want a very simple setup to get your feet wet, you can get Mithril from a CDN:

+
<script src="http://cdn.rawgit.com/lhorie/mithril.js/rewrite/mithril.js"></script>
+
+
+

NPM

+

Quick start with Webpack

+
# 1) install
+npm install mithril@rewrite --save
+
+npm install webpack --save
+
+# 2) add this line into the scripts section in package.json
+#    "scripts": {
+#        "start": "webpack src/index.js bin/app.js --watch"
+#    }
+
+# 3) create an `src/index.js` file
+
+# 4) create an `index.html` file containing `<script src="bin/app.js"></script>`
+
+# 5) run bundler
+npm start
+
+# 6) open `index.html` in the (default) browser
+open index.html
+
+

Step by step

+

For production-level projects, the recommended way of installing Mithril is to use NPM.

+

NPM (Node package manager) is the default package manager that is bundled w/ Node.js. It is widely used as the package manager for both client-side and server-side libraries in the Javascript ecosystem. Download and install Node.js; NPM will be automatically installed as well.

+

To use Mithril via NPM, go to your project folder, and run npm init --yes from the command line. This will create a file called package.json.

+
npm init --yes
+# creates a file called package.json
+
+

Then, to install Mithril, run:

+
npm install mithril@rewrite --save
+
+

This will create a folder called node_modules, and a mithril folder inside of it. It will also add an entry under dependencies in the package.json file

+

You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules:

+
// index.js
+var m = require("mithril")
+
+m.render(document.body, "hello world")
+
+

Modularization is the practice of separating the code into files. Doing so makes it easier to find code, understand what code relies on what code, and test.

+

CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like Browserify and Webpack. It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like Rollup, Babel or Traceur.

+

Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application.

+

A popular way for creating a bundle is to setup an NPM script for Webpack. To install Webpack, run this from the command line:

+
npm install webpack --save-dev
+
+

Open the package.json that you created earlier, and add an entry to the scripts section:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack src/index.js bin/app.js -d --watch"
+    }
+}
+

Remember this is a JSON file, so object key names such as "scripts" and "start" must be inside of double quotes.

+

The -d flag tells webpack to use development mode, which produces source maps for a better debugging experience.

+

The --watch flag tells webpack to watch the file system and automatically recreate app.js if file changes are detected.

+

Now you can run the script via npm start in your command line window. This looks up the webpack command in the NPM path, reads index.js and creates a file called app.js which includes both Mithril and the hello world code above. If you want to run the webpack command directly from the command line, you need to either add node_modules/.bin to your PATH, or install webpack globally via npm install webpack -g. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers.

+
npm start
+

Now that you have created a bundle, you can then reference the bin/app.js file from an HTML file:

+
<html>
+  <head>
+    <title>Hello world</title>
+  </head>
+  <body>
+    <script src="bin/app.js"></script>
+  </body>
+</html>
+
+

As you've seen above, importing a module in CommonJS is done via the require function. You can reference NPM modules by their library names (e.g. require("mithril") or require("jquery")), and you can reference your own modules via relative paths minus the file extension (e.g. if you have a file called mycomponent.js in the same folder as the file you're importing to, you can import it by calling require("./mycomponent")).

+

To export a module, assign what you want to export to the special module.exports object:

+
// mycomponent.js
+module.exports = {
+    view: function() {return "hello from a module"}
+}
+
+

In the index.js, you would then write this code to import that module:

+
// index.js
+var m = require("mithril")
+
+var MyComponent = require("./mycomponent")
+
+m.mount(document.body, MyComponent)
+
+

Note that in this example, we're using m.mount, which wires up the component to Mithril's autoredraw system. In most applications, you will want to use m.mount (or m.route if your application has multiple screens) instead of m.render to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs.

+

Production build

+

If you open bin/app.js, you'll notice that the Webpack bundle is not minified, so this file is not ideal for a live application. To generate a minified file, open package.json and add a new npm script:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack src/index.js bin/app.js -d --watch",
+        "build": "webpack src/index.js bin/app.js -p",
+    }
+}
+

You can use hooks in your production environment to run the production build script automatically. Here's an example for Heroku:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch",
+        "build": "webpack -p",
+        "heroku-postbuild": "webpack -p"
+    }
+}
+

+

Alternate ways to use Mithril

+

Live reload development environment

+

Live reload is a feature where code changes automatically trigger the page to reload. Budo is one tool that enables live reloading.

+
# 1) install
+npm install mithril@rewrite --save
+npm install budo -g
+
+# 2) add this line into the scripts section in package.json
+#    "scripts": {
+#        "start": "budo --live --open index.js"
+#    }
+
+# 3) create an `index.js` file
+
+# 4) run budo
+npm start
+
+

The source file index.js will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes.

+

Mithril bundler

+

Mithril comes with a bundler tool of its own. It is sufficient for ES5-based projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet.

+

If you want to try it and give feedback, you can open package.json and change the npm script for webpack to this:

+
{
+    "name": "my-project",
+    "scripts": {
+        "build": "bundle index.js --output app.js --watch"
+    }
+}
+

Vanilla

+

If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all:

+
<html>
+  <head>
+    <title>Hello world</title>
+  </head>
+  <body>
+    <script src="http://cdn.rawgit.com/lhorie/mithril.js/rewrite/mithril.js"></script>
+    <script src="index.js"></script>
+  </body>
+</html>
+
+
// index.js
+
+// if a CommonJS environment is not detected, Mithril will be created in the global scope
+m.render(document.body, "hello world")
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

jsonp(options)

+ +
+

Description

+

Makes JSON-P requests. Typically, it's useful to interact with servers that allow JSON-P but that don't have CORS enabled.

+
m.jsonp({
+    url: "/api/v1/users/:id",
+    data: {id: 1},
+    callbackKey: "callback",
+})
+.then(function(result) {
+    console.log(result)
+})
+
+
+

Signature

+

promise = m.jsonp([url,] options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringNoIf present, it's equivalent to having the option {url: url}. Values passed to the options argument override options set via this shorthand.
options.urlStringYesThe URL to send the request to. The URL may be either absolute or relative, and it may contain interpolations.
options.dataanyNoThe data to be interpolated into the URL and serialized into the querystring.
options.typeany = Function(any)NoA constructor to be applied to each object in the response. Defaults to the identity function.
options.callbackNameStringNoThe name of the function that will be called as the callback. Defaults to a randomized string (e.g. _mithril_6888197422121285_0({a: 1})
options.callbackKeyStringNoThe name of the querystring parameter name that specifies the callback name. Defaults to callback (e.g. /someapi?callback=_mithril_6888197422121285_0)
returnsPromiseA promise that resolves to the response data, after it has been piped through type method
+

How to read signatures

+
+

How it works

+

The m.jsonp utility is useful for third party APIs that can return data in JSON-P format.

+

In a nutshell, JSON-P consists of creating a script tag whose src attribute points to a script that lives in the server outside of your control. Typically, you are required to define a global function and specify its name in the querystring of the script's URL. The response will return code that calls your global function, passing the server's data as the first parameter.

+

JSON-P has several limitations: it can only use GET requests, it implicitly trusts that the third party server won't serve malicious code and it requires polluting the global Javascript scope. Nonetheless, it is sometimes the only available way to retrieve data from a service (for example, if the service doesn't support CORS).

+
+

Typical usage

+

Some services follow the de-facto convention of responding with JSON-P if a callback querystring key is provided, thus making m.jsonp automatically work without any effort:

+
m.jsonp({url: "https://api.github.com/users/lhorie"}).then(function(response) {
+    console.log(response.data.login) // logs "lhorie"
+})
+
+

Some services do not follow conventions and therefore you must specify the callback key that the service expects:

+
m.jsonp({
+    url: "https://api.flickr.com/services/feeds/photos_public.gne?tags=kitten&format=json",
+    callbackKey: "jsoncallback",
+})
+.then(function(response) {
+    console.log(response.link) // logs "https://www.flickr.com/photos/tags/kitten/"
+})
+
+

And sometimes, you just want to take advantage of HTTP caching for GET requests for rarely-modified data:

+
// this request is always called with the same querystring, and therefore it is cached
+m.jsonp({
+    url: "https://api.github.com/users/lhorie",
+    callbackName: "__callback",
+})
+.then(function(response) {
+    console.log(response.data.login) // logs "lhorie"
+})
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

JSX

+ +
+

Description

+

JSX is a syntax extension that enables you to write HTML tags interspersed with Javascript. It's not part of any Javascript standards and it's not required for building applications, but it may be more pleasing to use depending on your team's preferences.

+
var MyComponent = {
+  view: function() {
+    return m("main", [
+      m("h1", "Hello world"),
+    ])
+  }
+}
+
+// can be written as:
+var MyComponent = {
+  view: function() {
+    return (
+      <main>
+        <h1>Hello world</h1>
+      </main>
+    )
+  }
+}
+
+

When using JSX, it's possible to interpolate Javascript expressions within JSX tags by using curly braces:

+
var greeting = "Hello"
+var url = "http://google.com"
+var link = <a href={url}>{greeting + "!"}</a>
+// yields <a href="http://google.com">Hello</a>
+
+

Components can be used by using a convention of uppercasing the first letter of the component name:

+
m.mount(document.body, <MyComponent />)
+// equivalent to m.mount(document.body, m(MyComponent))
+
+
+

Setup

+

The simplest way to use JSX is via a Babel plugin.

+

Babel requires NPM, which is automatically installed when you install Node.js. Once NPM is installed, create a project folder and run this command:

+
npm init -y
+
+

If you want to use Webpack and Babel together, skip to the section below.

+

To install Babel as a standalone tool, use this command:

+
npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev
+
+

Create a .babelrc file:

+
{
+    "presets": ["es2015"],
+    "plugins": [
+        ["transform-react-jsx", {
+            "pragma": "m"
+        }]
+    ]
+}
+

To run Babel as a standalone tool, run this from the command line:

+
babel src --out-dir bin --source-maps
+
+

Using Babel with Webpack

+

If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps.

+
npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev
+
+

Create a .babelrc file:

+
{
+    "presets": ["es2015"],
+    "plugins": [
+        ["transform-react-jsx", {
+            "pragma": "m"
+        }]
+    ]
+}
+

Next, create a file called webpack.config.js

+
module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: './bin',
+        filename: 'app.js',
+    },
+    module: {
+        loaders: [{
+            test: /\.js$/,
+            exclude: /node_modules/,
+            loader: 'babel-loader'
+        }]
+    }
+}
+
+

This configuration assumes the source code file for the application entry point is in src/index.js, and this will output the bundle to bin/app.js.

+

To run the bundler, setup an npm script. Open package.json and add this entry under "scripts":

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch"
+    }
+}
+

You can now then run the bundler by running this from the command line:

+
npm start
+
+

Production build

+

To generate a minified file, open package.json and add a new npm script called build:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch",
+        "build": "webpack -p",
+    }
+}
+

You can use hooks in your production environment to run the production build script automatically. Here's an example for Heroku:

+
{
+    "name": "my-project",
+    "scripts": {
+        "start": "webpack -d --watch",
+        "build": "webpack -p",
+        "heroku-postbuild": "webpack -p"
+    }
+}
+

+

JSX vs hyperscript

+

JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and can make syntax issues difficult to spot.

+

Unlike HTML, JSX is case-sensitive. This means <div className="test"></div> is different from <div classname="test"></div> (all lower case). The former compiles to m("div", {className: "test"}) and the latter compiles to m("div", {classname: "test"}), which is not a valid way of creating a class attribute. Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: <div class="test"></div>.

+

JSX is useful for teams where HTML is primarily written by someone without Javascript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser)

+

Hyperscript is the compiled representation of JSX. It's designed to be readable and can also be used as-is, instead of JSX (as is done in most of the documentation). Hyperscript tends to be terser than JSX for a couple of reasons:

+
    +
  • it does not require repeating the tag name in closing tags (e.g. m("div") vs <div></div>)
  • +
  • static attributes can be written using CSS selector syntax (i.e. m("a.button") vs <div class="button"></div>
  • +
+

In addition, since hyperscript is plain Javascript, it's often more natural to indent than JSX:

+
//JSX
+var BigComponent = {
+  activate: function() {/*...*/},
+  deactivate: function() {/*...*/},
+  update: function() {/*...*/},
+  view: function(vnode) {
+    return [
+      {vnode.attrs.items.map(function(item) {
+        return <div>{item.name}</div>
+      })}
+      <div
+        ondragover={this.activate}
+        ondragleave={this.deactivate}
+        ondragend={this.deactivate}
+        ondrop={this.update}
+        onblur={this.deactivate}
+      ></div>
+    ]
+  }
+}
+
+// hyperscript
+var BigComponent = {
+  activate: function() {/*...*/},
+  deactivate: function() {/*...*/},
+  update: function() {/*...*/},
+  view: function(vnode) {
+    return [
+      vnode.attrs.items.map(function(item) {
+        return m("div", item.name)
+      }),
+      m("div", {
+        ondragover: this.activate,
+        ondragleave: this.deactivate,
+        ondragend: this.deactivate,
+        ondrop: this.update,
+        onblur: this.deactivate,
+      })
+    ]
+  }
+}
+
+

In non-trivial applications, it's possible for components to have more control flow and component configuration code than markup, making a Javascript-first approach more readable than an HTML-first approach.

+

Needless to say, since hyperscript is pure Javascript, there's no need to run a compilation step to produce runnable code.

+
+

Converting HTML

+

In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX.

+

When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can use the HTML-to-Mithril-template converter.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Keys

+ +
+

What are keys

+

Keys are a mechanism that allows re-ordering DOM elements within a NodeList, and mapping specific data items in a list to the respective DOM elements that are derived from them, as the data items move within the list.

+

In other words, a key is a way of saying "this DOM element is for the data object with this id".

+

Typically, a key property should be the unique identifier field of the objects in the data array.

+
var users = [
+    {id: 1, name: "John"},
+    {id: 2, name: "Mary"},
+]
+
+function userInputs(users) {
+    return users.map(function(u) {
+        return m("input", {key: u.id}, u.name)
+    })
+}
+
+m.render(document.body, userInputs(users))
+
+

Having a key means that if the users array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.

+
+

How to use

+

A common pattern is to have data comprised of an array of objects and to generate a list of vnodes that map to each object in the array. For example, consider the following code:

+
var people = [
+    {id: 1, name: "John"},
+    {id: 2, name: "Mary"},
+]
+
+function userList(users) {
+    return users.map(function(u) {
+        return m("button", u.name) // <button>John</button>
+                                   // <button>Mary</button>
+    })
+}
+
+m.render(document.body, userList(people))
+
+

Let's suppose the people variable was changed to this:

+
people = [{id: 2, name: "Mary"}]
+
+

The problem is that from the point of view of the userList function, there's no way to tell if it was the first object that was removed, or if it was the second object that was removed in addition to the first object's properties being modified. If the first button was focused and the rendering engine removes it, then focus goes back to <body> as expected, but if the rendering engine removes the second button and modifies the text content of the first, then the focus will be on the wrong button after the update.

+

Worse still, if there were stateful jQuery plugins attached to these buttons, they could potentially have incorrect internal state after the update.

+

Even though in this particular example, we humans intuitively guess that the first item in the list was the one being removed, it's actually impossible for a computer to automatically solve this problem for all possible inputs.

+

Therefore, in the cases when a list of vnodes is derived from a dynamic array of data, you should add a key property to each virtual node that maps to a uniquely identifiable field in the source data. This will allow Mithril to intelligently re-order the DOM to maintain each DOM element correctly mapped to its respective item in the data source.

+
function correctUserList(users) {
+    return users.map(function(u) {
+        return m("button", {key: u.id}, u.name)
+    })
+}
+
+
+ +

Keys can cause confusing issues if they are misunderstood. A typical symptom of key related issues is that application state appears to become corrupted after a few user interactions (usually involving a deletion).

+

Avoid wrapper elements around keyed elements

+

Keys must be placed on the virtual node that is an immediate child of the array. This means that if you wrap the button in an div in the example above, the key must be moved to the div.

+
// AVOID
+users.map(function(u) {
+    return m("div", [ // key should be in `div`
+        m("button", {key: u.id}, u.name)
+    ])
+})
+
+

Avoid hiding keys in component root elements

+

If you refactor the code and put the button inside a component, the key must be moved out of the component and placed back where the component took the place of the button.

+
// AVOID
+var Button = {
+    view: function(vnode) {
+        return m("button", {key: vnode.attrs.id}, u.name)
+    }
+}
+users.map(function(u) {
+    return m("div", [
+        m(Button, {id: u.id}, u.name) // key should be here, not in component
+    ])
+})
+
+

Avoid wrapping keyed elements in arrays

+

Arrays are vnodes, and therefore keyable. You should not wrap arrays around keyed elements

+
// AVOID
+users.map(function(u) {
+    return [ // fragment is a vnode, and therefore keyable
+        m("button", {key: u.id}, u.name)
+    ]
+})
+
+// PREFER
+users.map(function(u) {
+    return m("button", {key: u.id}, u.name)
+})
+
+// PREFER
+users.map(function(u) {
+    return m.fragment({key: u.id}, m("button", u.name))
+})
+
+

Avoid variable types

+

Keys must be strings if present or they will be cast to strings if they are not. Therefore, "1" (string) and 1 (number) are considered the same key.

+

You should use either strings or numbers as keys in one array, but not mix both.

+
// AVOID
+var things = [
+    {id: "1", name: "Book"},
+    {id: 1, name: "Cup"},
+]
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril [version]

+ +
+
+
+
+ [body] +
+ License: MIT. © Leo Horie. +
+
+ + +e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(c?b[1].length:0),_=b.index+b[0].length,A=m,P=y,j=r.length;j>A&&_>P;++A)P+=r[A].length,w>=P&&(++m,y=P);if(r[m]instanceof a||r[A-1].greedy)continue;k=A-m,v=e.slice(y,P),b.index-=y}if(b){c&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),S=[m,k];x&&S.push(x);var N=new a(l,g?n.tokenize(b,g):b,d,b,h);S.push(N),O&&S.push(O),Array.prototype.splice.apply(r,S)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var i={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}n.hooks.run("wrap",i);var o=Object.keys(i.attributes).map(function(e){return e+'="'+(i.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,i=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),i&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; +Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:{pattern:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\w\W]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css"}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\\\|\\?[^\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; +!function(a){var e=a.util.clone(a.languages.javascript);a.languages.jsx=a.languages.extend("markup",e),a.languages.jsx.tag.pattern=/<\/?[\w\.:-]+\s*(?:\s+[\w\.:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+|(\{[\w\W]*?\})))?\s*)*\/?>/i,a.languages.jsx.tag.inside["attr-value"].pattern=/=[^\{](?:('|")[\w\W]*?(\1)|[^\s>]+)/i;var s=a.util.clone(a.languages.jsx);delete s.punctuation,s=a.languages.insertBefore("jsx","operator",{punctuation:/=(?={)|[{}[\];(),.:]/},{jsx:s}),a.languages.insertBefore("inside","attr-value",{script:{pattern:/=(\{(?:\{[^}]*\}|[^}])+\})/i,inside:s,alias:"language-javascript"}},a.languages.jsx.tag)}(Prism); diff --git a/archive/v1.0.0-rc.8/lifecycle-methods.html b/archive/v1.0.0-rc.8/lifecycle-methods.html new file mode 100644 index 00000000..eee254c2 --- /dev/null +++ b/archive/v1.0.0-rc.8/lifecycle-methods.html @@ -0,0 +1,208 @@ + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Lifecycle methods

+ +
+

Usage

+

Components and virtual DOM nodes can have lifecycle methods, also known as hooks, which are called at various points during the lifetime of a DOM element.

+
// Sample hook in component
+var ComponentWithHook = {
+    oninit: function(vnode) {
+        console.log("initialize component")
+    },
+    view: function() {
+        return "hello"
+    }
+}
+
+// Sample hook in vnode
+function initializeVnode() {
+    console.log("initialize vnode")
+}
+
+m(ComponentWithHook, {oninit: initializeVnode})
+
+

All lifecyle methods receive the vnode as their first arguments, and have their this keyword bound to vnode.state.

+

Lifecycle methods are only called as a side effect of a m.render() call. They are not called if the DOM is modified outside of Mithril.

+
+

The DOM element lifecycle

+

A DOM element is typically created and appended to the document. It may then have attributes or child nodes updated when a UI event is triggered and data is changed; and the element may alternatively be removed from the document.

+

After an element is removed, it may be temporarily retained in a memory pool. The pooled element may be reused in a subsequent update (in a process called DOM recycling). Recycling an element avoids incurring the performance cost of recreating a copy of an element that existed recently.

+
+

oninit

+

The oninit(vnode) hook is called before a vnode is touched by the virtual DOM engine. oninit is guaranteed to run before its DOM element is attached to the document, and it is guaranteed to run on parent vnodes before their children, but it does not offer any guarantees regarding the existence of ancestor or descendant DOM elements. You should never access the vnode.dom from the oninit method.

+

This hook does not get called when an element is updated, but it does get called if an element is recycled.

+

Like in other hooks, the this keyword in the oninit callback points to vnode.state.

+

The oninit hook is useful for initializing component state based on arguments passed via vnode.attrs or vnode.children.

+
var ComponentWithState = {
+    oninit: function(vnode) {
+        this.data = vnode.attrs.data
+    },
+    view: function() {
+        return m("div", this.data) // displays data from initialization time
+    }
+}
+
+m(ComponentWithState, {data: "Hello"})
+
+// Equivalent HTML
+// <div>Hello</div>
+
+

You should not modify model data synchronously from this method. Since oninit makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle.

+
+

oncreate

+

The oncreate(vnode) hook is called after a DOM element is created and attached to the document. oncreate is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as vnode.dom.offsetHeight and vnode.dom.getBoundingClientRect() from this method.

+

This hook does not get called when an element is updated.

+

Like in other hooks, the this keyword in the oncreate callback points to vnode.state. DOM elements whose vnodes have an oncreate hook do not get recycled.

+

The oncreate hook is useful for reading layout values that may trigger a repaint, starting animations and for initializing third party libraries that require a reference to the DOM element.

+
var HeightReporter = {
+    oncreate: function(vnode) {
+        console.log("Initialized with height of: ", vnode.dom.offsetHeight)
+    },
+    view: function() {}
+}
+
+m(HeightReporter, {data: "Hello"})
+
+

You should not modify model data synchronously from this method. Since oncreate is run at the end of the render cycle, model changes created from this method will not be reflected in the UI until the next render cycle.

+
+

onupdate

+

The onupdate(vnode) hook is called after a DOM element is updated, while attached to the document. onupdate is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as vnode.dom.offsetHeight and vnode.dom.getBoundingClientRect() from this method.

+

This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled.

+

Like in other hooks, the this keyword in the onupdate callback points to vnode.state. DOM elements whose vnodes have an onupdate hook do not get recycled.

+

The onupdate hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed.

+
var RedrawReporter = {
+    count: 0,
+    onupdate: function(vnode) {
+        console.log("Redraws so far: ", ++vnode.state.count)
+    },
+    view: function() {}
+}
+
+m(RedrawReporter, {data: "Hello"})
+
+
+

onbeforeremove

+

The onbeforeremove(vnode) hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes.

+

This hook is only called on the DOM element that loses its parentNode, but it does not get called in its child elements.

+

Like in other hooks, the this keyword in the onbeforeremove callback points to vnode.state. DOM elements whose vnodes have an onbeforeremove hook do not get recycled.

+
var Fader = {
+    onbeforeremove: function(vnode) {
+        vnode.dom.classList.add("fade-out")
+        return new Promise(function(resolve) {
+            setTimeout(resolve, 1000)
+        })
+    },
+    view: function() {
+        return m("div", "Bye")
+    },
+}
+
+
+

onremove

+

The onremove(vnode) hook is called before a DOM element is removed from the document. If a onbeforeremove hook is also defined, the onremove hook runs after the promise returned from onbeforeremove is completed.

+

This hook is called on any element that is removed from the document, regardless of whether it was directly detached from its parent or whether it is a child of another element that was detached.

+

Like in other hooks, the this keyword in the onremove callback points to vnode.state. DOM elements whose vnodes have an onremove hook do not get recycled.

+

The onremove hook is useful for running clean up tasks.

+
var Timer = {
+    oninit: function(vnode) {
+        this.timeout = setTimeout(function() {
+            console.log("timed out")
+        }, 1000)
+    },
+    onremove: function(vnode) {
+        clearTimeout(this.timeout)
+    },
+    view: function() {}
+}
+
+
+

onbeforeupdate

+

The onbeforeupdate(vnode, old) hook is called before a vnode is diffed in a update. If this function is defined and returns false, Mithril prevents a diff from happening to the vnode, and consequently to the vnode's children.

+

This hook by itself does not prevent a virtual DOM subtree from being generated unless the subtree is encapsulated within a component.

+

Like in other hooks, the this keyword in the onbeforeupdate callback points to vnode.state.

+

This hook is useful to reduce lag in updates in cases where there is a overly large DOM tree.

+
+

Avoid anti-patterns

+

Although Mithril is flexible, some code patterns are discouraged:

+

Avoid premature optimizations

+

You should only use onbeforeupdate to skip diffing as a last resort. Avoid using it unless you have a noticeable performance issue.

+

Typically performance problems that can be fixed via onbeforeupdate boil down to one large array of items. In this context, typically "large" means any array that contains a large number of nodes, be it in a wide spread (the infamous 5000 row table), or in a deep, dense tree.

+

If you do have a performance issue, first consider whether the UI presents a good user experience and change it if it doesn't. For example, it's highly unlikely that a user would ever sift through 5000 rows of raw table data, and highly likely that it would be easier for a user to use a search feature that returns only the top few most relevant items.

+

If a design-based solution is not feasible, and you must optimize a UI with a large number of DOM element, apply onbeforeupdate on the parent node of the largest array and re-evaluate performance. In the vast majority of cases, a single check should be sufficient. In the rare case that it is not, rinse and repeat, but you should be increasingly wary of each new onbeforeupdate declaration. Multiple onbeforeupdates are a code smell that indicates prioritization problems in the design workflow.

+

Avoid applying the optimization to other areas of your application "just-in-case". Remember that, generally speaking, more code incurs a higher maintenance cost than less code, and onbeforeupdate related bugs can be especially difficult to troubleshoot if you rely on object identity for its conditional checks.

+

Again, the onbeforeupdate hook should only be used as a last resort.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

mount(root, component)

+ +
+

Description

+

Activates a component, enabling it to autoredraw on user events

+
var state = {
+    count: 0,
+    inc: function() {state.count++}
+}
+
+var Counter = {
+    view: function() {
+        return m("div", {onclick: state.inc}, state.count)
+    }
+}
+
+m.mount(document.body, Counter)
+
+
+

Signature

+

m.mount(element, component)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
elementElementYesA DOM element that will be the parent node to the subtree
componentComponent|nullYesThe component to be rendered. null unmounts the tree and cleans up internal state.
returnsReturns nothing
+

How to read signatures

+
+

How it works

+

Similar to m.render(), the m.mount() method takes a component and mounts a corresponding DOM tree into element. If element already has a DOM tree mounted via a previous m.mount() call, the component is diffed against the previous vnode tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all.

+

Replace a component

+

Running mount(element, OtherComponent) where element is a current mount point replaces the component previously mounted with OtherComponent.

+

Unmount

+

Using m.mount(element, null) on an element with a previously mounted component unmounts it and cleans up Mithril internal state. This can be useful to prevent memory leaks when removing the root node manually from the DOM.

+
+

Performance considerations

+

It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing Javascript data structures is surprisingly cheap compared to reading and modifying the DOM.

+

Touching the DOM can be extremely expensive for a couple of reasons. Alternating reads and writes can adversely affect performance by causing several browser repaints to occur in quick succession, whereas comparing virtual dom trees allows writes to be batched into a single repaint. Also, the performance characteristics of various DOM operations vary between implementations and can be difficult to learn and optimize for all browsers. For example, in some implementations, reading childNodes.length has a complexity of O(n); in some, reading parentNode causes a repaint, etc.

+

In contrast, traversing a javascript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern javascript engines to apply aggressive optimizations such as hidden classes for even better performance.

+
+

Differences from m.render

+

A component rendered via m.mount automatically auto-redraws in response to view events, m.redraw() calls or m.request() calls. Vnodes rendered via m.render() do not. Note that calls to m.prop() do not trigger auto-redraws.

+

m.mount() is suitable for application developers integrating Mithril widgets into existing codebases where routing is handled by another library or framework, while still enjoying Mithril's auto-redrawing facilities.

+

m.render() is suitable for library authors who wish to manually control rendering (e.g. when hooking to a third party router, or using third party data-layer libraries like Redux).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

parseQueryString(string)

+ +
+

Description

+

Turns a string of the form ?a=1&b=2 to an object

+
var object = m.parseQueryString("a=1&b=2")
+// {a: "1", b: "2"}
+
+
+

Signature

+

object = m.parseQueryString(string)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
stringStringYesA querystring
returnsObjectA key-value map
+

How to read signatures

+
+

How it works

+

The m.parseQueryString method creates an object from a querystring. It is useful for handling data from URL

+
var data = m.parseQueryString("a=hello&b=world")
+
+// data is {a: "hello", b: "world"}
+
+

Boolean type casting

+

This method attempts to cast boolean values if possible. This helps prevents bugs related to loose truthiness and unintended type casts.

+
var data = m.parseQueryString("a=true&b=false")
+
+// data is {a: true, b: false}
+
+

Leading question mark tolerance

+

For convenience, the m.parseQueryString method ignores a leading question mark, if present:

+
var data = m.parseQueryString("?a=hello&b=world")
+
+// data is {a: "hello", b: "world"}
+
+

Deep data structures

+

Querystrings that contain bracket notation are correctly parsed into deep data structures

+
m.parseQueryString("a[0]=hello&a[1]=world")
+
+// data is {a: ["hello", "world"]}
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Promise(executor)

+ +
+

Description

+

A ES6 Promise polyfill.

+

A Promise is a mechanism for working with asynchronous computations.

+
+

Signature

+

promise = new Promise(executor)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
executor(Function, Function) -> anyYesA function that determines how the promise will be resolved or rejected
returnsPromiseReturns a promise
+

How to read signatures

+
+
executor
+

executor(resolve, reject)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
resolveany -> anyNoCall this function to resolve the promise
rejectany -> anyNoCall this function to reject the promise
returnsThe return value is ignored
+

How to read signatures

+
+

Static members

+
Promise.resolve
+

promise = Promise.resolve(value)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
valueanyNoA value to resolve to
returnsPromiseA promise resolved to value
+

How to read signatures

+
+
Promise.reject
+

promise = Promise.reject(value)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
valueanyNoA value to reject as
returnsPromiseA rejected promise with value as its reason
+

How to read signatures

+
+
Promise.all
+

promise = Promise.all(promises)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
promisesArray<Promise|any>YesA list of promises to wait for. If an item is not a promise, it's equivalent to calling Promise.resolve on it
returnsPromiseA promise that resolves only after all promises resolve, or rejects if any of them are rejected.
+

How to read signatures

+
+
Promise.race
+

promise = Promise.race(promises)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
promisesArray<Promise|any>YesA list of promises to wait for. If an item is not a promise, it's equivalent to calling Promise.resolve on it
returnsPromiseA promise that resolves as soon as one of the promises is resolved or rejected.
+

How to read signatures

+
+

Instance members

+
promise.then
+

nextPromise = promise.then(onFulfilled, onRejected)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
onFulfilledany -> (any|Promise)NoA function that is called if the promise is resolved. The first parameter of this function is the value that this promise was resolved with. If the return value of this function is not a Promise, it is used as the value for resolving nextPromise. If the returned value is a Promise, the value of nextPromise depends on the inner Promise's status. If this function throws, nextPromise is rejected with the error as its reason. If onFulfilled is null, it's ignored
onRejectedany -> (any|Promise)NoA function that is called when the promise is rejected. The first parameter of this function is a value that represents the reason why the promise was rejected. If the return value of this function is not a Promise, it is used as the value for resolving nextPromise. If the returned value is a Promise, then value of nextPromise depends on the inner Promise's status. If this function throws, nextPromise is rejected with the error as its reason. If onRejected is null, it's ignored
returnsPromiseA promise whose value depends on the status of the current promise
+

How to read signatures

+
+
promise.catch
+

nextPromise = promise.catch(onRejected)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
onRejectedany -> (any|Promise)NoA function that is called when the promise is rejected. The first parameter of this function is a value that represents the reason why the promise was rejected. If the return value of this function is not a Promise, it is used as the value for resolving nextPromise. If the returned value is a Promise, then value of nextPromise depends on the inner Promise's status. If this function throws, nextPromise is rejected with the error as its reason. If onRejected is null, it's ignored
returnsPromiseA promise whose value depends on the status of the current promise
+

How to read signatures

+
+

How it works

+

A Promise is an object that represents a value which may be available in the future

+
// this promise resolves after one second
+var promise = new Promise(function(resolve, reject) {
+  setTimeout(function() {
+    resolve("hello")
+  }, 1000)
+})
+
+promise.then(function(value) {
+  // logs "hello" after one second
+  console.log(value)
+})
+
+

Promises are useful for working with asynchronous APIs, such as m.request

+

Asynchronous APIs are those which typically take a long time to run, and therefore would take too long to return a value using the return statement of a function. Instead, they do their work in the background, allowing other Javascript code to run in the meantime. When they are done, they call a function with their results.

+

The m.request function takes time to run because it makes an HTTP request to a remote server and has to wait for a response, which may take several milliseconds due to network latency.

+
+

Promise chaining

+

Promises can be chained. Returning a value from a then callback makes it available as the argument to the next then callback. This allows refactoring code into smaller functions

+
function getUsers() {return m.request("/api/v1/users")}
+
+// AVOID: hard to test god functions
+getUsers().then(function(users) {
+  var firstTen = users.slice(0, 9)
+  var firstTenNames = firstTen.map(function(user) {return user.firstName + " " + user.lastName})
+  alert(firstTenNames)
+})
+
+// PREFER: easy to test small functions
+function getFirstTen(items) {return items.slice(0, 9)}
+function getUserName(user) {return user.firstName + " " + user.lastName}
+function getUserNames(users) {return users.map(getUserName)}
+
+getUsers()
+  .then(getFirstTen)
+  .then(getUserNames)
+  .then(alert)
+
+

In the refactored code, getUsers() returns a promise, and we chain three callbacks. When getUsers() resolves, the getFirstTen function is called with a list of users as its first argument. This function returns a list of ten items. getUserNames returns a list of names for the 10 items that were passed as the argument to it. Finally, the list of names is alerted.

+

In the original code above, it's very difficult to test the god function since you must make an HTTP request to run the code, and there's an alert() call at the end of the function

+

In the refactored version, it's trivial to test whether getFirstTen has any off-by-one errors, or whether we forgot to add a space between the first and last names in getUserName.

+
+

Promise absorption

+

Promises absorb other promises. Basically, this means you can never receive a Promise as an argument to onFulfilled or onRejected callbacks for then and catch methods. This feature allows us to flatten nested promises to make code more manageable.

+
function searchUsers(q) {return m.request("/api/v1/users/search", {data: {q: q}})}
+function getUserProjects(id) {return m.request("/api/v1/users/" + id + "/projects")}
+
+// AVOID: pyramid of doom
+searchUsers("John").then(function(users) {
+  getUserProjects(users[0].id).then(function(projects) {
+    var titles = projects.map(function(project) {return project.title})
+    alert(titles)
+  })
+})
+
+// PREFER: flat code flow
+function getFirstId(items) {return items[0].id}
+function getProjectTitles(projects) {return projects.map(getProjectTitle)}
+function getProjectTitle(project) {return project.title}
+
+searchUsers("John")
+  .then(getFirstId)
+  .then(getUserProjects)
+  .then(getProjectTitles)
+  .then(alert)
+
+

In the refactored code, getFirstId returns an id, which is passed as the first argument to getUserProjects. That, in turn, returns a promise that resolves to a list of projects. This promise is absorbed, so the first argument to getProjectTitles is not a promise, but the list of projects. getProjectTitles returns a list of titles, and that list is finally alerted.

+
+

Error handling

+

Promises can propagate errors to appropriate handlers.

+
searchUsers("John")
+  .then(getFirstId)
+  .then(getUserProjects)
+  .then(getProjectTitles)
+  .then(alert)
+  .catch(function(e) {
+    console.log(e)
+  })
+
+

Here's the previous example with error handling. The searchUsers function could fail if the network was offline, resulting in an error. In that case, none of the .then callbacks would be triggered, and the .catch callback would log the error to console.

+

If the request in getUserProjects failed, then similarly, getProjectTitles and alert would not be called. Again, the .catch callback would log the error.

+

The error handler would also catch a null reference exception if searchUsers returned no results, and getFirstId attempted to access the id property of a non-existent array item.

+

Thanks to these error propagation semantics, it's easy to keep each function small and testable without sprinkling try/catch blocks everywhere.

+
+

Shorthands

+

Sometimes, you already have a value, but want to wrap it in a Promise. It's for this purpose that Promise.resolve and Promise.reject exist.

+
// suppose this list came from localStorage
+var users = [{id: 1, firstName: "John", lastName: "Doe"}]
+
+// in that case, `users` may or may not exist depending on whether there was data in localStorage
+var promise = users ? Promise.resolve(users) : getUsers()
+promise
+  .then(getFirstTen)
+  .then(getUserNames)
+  .then(alert)
+
+
+

Multiple promises

+

In some occasions, you may need to make HTTP requests in parallel, and run code after all requests complete. This can be accomplished by Promise.all

+
Promise.all([
+  searchUsers("John"),
+  searchUsers("Mary"),
+])
+.then(function(data) {
+  // data[0] is an array of users whose names are John
+  // data[1] is an array of users whose names are Mary
+
+  // the returned value is equivalent to [
+  //   getUserNames(data[0]),
+  //   getUserNames(data[1]),
+  // ]
+  return data.map(getUserNames)
+})
+.then(alert)
+
+

In the example above, there are two user searches happening in parallel. Once they both complete, we take the names of all the users and alert them.

+

This example also illustrates another benefit of smaller functions: we reused the getUserNames function we had created above.

+
+

Why not callbacks

+

Callbacks are another mechanism for working with asynchrounous computations, and are indeed more adequate to use if an asynchronous computation may occur more than one time (for example, an onscroll event handler).

+

However, for asynchronous computations that only occur once in response to an action, promises can be refactored more effectively, reducing code smells known as pyramids of doom (deeply nested series of callbacks with unmanaged state being used across several closure levels).

+

In addition, promises can considerably reduce boilerplate related to error handling.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

redraw()

+ +
+

Description

+

Updates the DOM after a change in the application data layer.

+

You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril view, or after request completion when using m.request/m.jsonp.

+

You DO need to call it in setTimeout/setInterval/requestAnimationFrame callbacks, or callbacks from 3rd party libraries.

+

Typically, m.redraw triggers an asynchronous redraws, but it may trigger synchronously if Mithril detects it's possible to improve performance by doing so (i.e. if no redraw was requested within the last animation frame). You should write code assuming that it always redraws asynchronously.

+
+

Signature

+

m.redraw()

+ + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
returnsReturns nothing
+
+

How it works

+

When callbacks outside of Mithril run, you need to notify Mithril's rendering engine that a redraw is needed. External callbacks could be setTimeout/setInterval/requestAnimationFrame callbacks, web socket library callbacks, event handlers in jQuery plugins, third party XHR request callbacks, etc.

+

To trigger a redraw, call m.redraw(). Note that m.redraw only works if you used m.mount or m.route. If you rendered via m.render, you should use m.render to redraw.

+

You should not call m.redraw from a lifecycle method. Doing so will result in undefined behavior.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Releasing

+ +

Releasing new builds of mithril is mostly automated via npm version

+

Publishing to NPM

+
    +
  1. npm version <major|minor|patch|semver> -m "v%s"
  2. +
+

All further steps are automated and run as follows:

+
    +
  1. Tests are run
  2. +
  3. Linting is run (but doesn't fail build)
  4. +
  5. Version number in package.json is incremented
  6. +
  7. New bundles are generated using updated version
  8. +
  9. git add called on bundle output
  10. +
  11. package.json and updated bundles are committed to git
  12. +
  13. previous commit is tagged using new version number
  14. +
  15. git push --follow-tags pushes up new version commit & tag to github
  16. +
  17. Travis sees new release, starts build
  18. +
  19. Travis generates new bundles before running tests
  20. +
  21. Travis runs tests
  22. +
  23. Travis lints files (but can't fail build)
  24. +
  25. If build fails, abort
  26. +
  27. Build succeeded, so travis will commit back any changes to the repo (but there won't be any)
  28. +
  29. Travis sees that this commit has a tag associated with it
  30. +
  31. Travis will use the encrypted npm creds in .travis.yml to publish a new version to npm
  32. +
+

Publishing a GitHub release

+

TODO

+

Updating docs/change-log.md

+

TODO

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

render(element, vnodes)

+ +
+

Description

+

Renders a template to the DOM

+
m.render(document.body, "hello")
+// <body>hello</body>
+
+
+

Signature

+

m.render(element, vnodes)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
elementElementYesA DOM element that will be the parent node to the subtree
vnodesArray<Vnode>|VnodeYesThe vnodes to be rendered
returnsReturns undefined
+

How to read signatures

+
+

How it works

+

The m.render(element, vnodes) method takes a virtual DOM tree (typically generated via the m() hyperscript function, generates a DOM tree and mounts it on element. If element already has a DOM tree mounted via a previous m.render() call, vnodes is diffed against the previous vnodes tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all.

+

m.render is synchronous.

+
+

Why Virtual DOM

+

It may seem wasteful to generate a vnode tree on every redraw, but as it turns out, creating and comparing Javascript data structures is surprisingly cheap compared to reading and modifying the DOM.

+

Touching the DOM can be extremely expensive for a couple of reasons. Alternating reads and writes can adversely affect performance by causing several browser repaints to occur in quick succession, whereas comparing virtual dom trees allows writes to be batched into a single repaint. Also, the performance characteristics of various DOM operations vary between implementations and can be difficult to learn and optimize for all browsers. For example, in some implementations, reading childNodes.length has a complexity of O(n); in some, reading parentNode causes a repaint, etc.

+

In contrast, traversing a javascript data structure has a much more predictable and sane performance profile, and in addition, a vnode tree is implemented in such a way that enables modern javascript engines to apply aggressive optimizations such as hidden classes for even better performance.

+
+

Differences from other API methods

+

m.render() method is internally called by m.mount(), m.route(), m.redraw() and m.request(). It is not called after stream updates

+

Unlike with m.mount() and m.route(), a vnode tree rendered via m.render() does not auto-redraw in response to view events, m.redraw() calls or m.request() calls. It is a low level mechanism suitable for library authors who wish to manually control rendering instead of relying on Mithril's built-in auto-redrawing system.

+

Another difference is that m.render method expects a vnode (or a array of vnodes) as its second parameter, whereas m.mount() and m.route() expect components.

+
+

Standalone usage

+

var render = require("mithril/render")

+

The m.render module is similar in scope to view libraries like Knockout, React and Vue. It is approximately 500 lines of code (3kb min+gzip) and implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril and can be used as a standalone library.

+

Despite being incredibly small, the render module is fully functional and self-suficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports components and lifecycle methods.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

request(options)

+ +
+

Description

+

Makes XHR (aka AJAX) requests, and returns a promise

+
m.request({
+    method: "PUT",
+    url: "/api/v1/users/:id",
+    data: {id: 1, name: "test"}
+})
+.then(function(result) {
+    console.log(result)
+})
+
+
+

Signature

+

promise = m.request([url,] options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringNoIf present, it's equivalent to having the options {method: "GET", url: url}. Values passed to the options argument override options set via this shorthand.
options.methodStringNoThe HTTP method to use. This value should be one of the following: GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS. Defaults to GET.
options.urlStringYesThe URL to send the request to. The URL may be either absolute or relative, and it may contain interpolations.
options.dataanyNoThe data to be interpolated into the URL and serialized into the querystring (for GET requests) or body (for other types of requests).
options.asyncBooleanNoWhether the request should be asynchronous. Defaults to true.
options.userStringNoA username for HTTP authorization. Defaults to undefined.
options.passwordStringNoA password for HTTP authorization. Defaults to undefined. This option is provided for XMLHttpRequest compatibility, but you should avoid using it because it sends the password in plain text over the network.
options.withCredentialsBooleanNoWhether to send cookies to 3rd party domains. Defaults to false
options.configxhr = Function(xhr)NoExposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the identity function.
options.headersObjectNoHeaders to append to the request before sending it (applied right before options.config).
options.typeany = Function(any)NoA constructor to be applied to each object in the response. Defaults to the identity function.
options.serializestring = Function(any)NoA serialization method to be applied to data. Defaults to JSON.stringify, or if options.data is an instance of FormData, defaults to the identity function (i.e. function(value) {return value}).
options.deserializeany = Function(string)NoA deserialization method to be applied to the response. Defaults to a small wrapper around JSON.parse that returns null for empty responses.
options.extractstring = Function(xhr, options)NoA hook to specify how the XMLHttpRequest response should be read. Useful for reading response headers and cookies. Defaults to a function that returns xhr.responseText. If defined, the xhr parameter is the XMLHttpRequest instance used for the request, and options is the object that was passed to the m.request call. If a custom extract callback is set, options.deserialize is ignored and the string returned from the extract callback will not automatically be parsed as JSON.
options.useBodyBooleanNoForce the use of the HTTP body section for data in GET requests when set to true, or the use of querystring for other HTTP methods when set to false. Defaults to false for GET requests and true for other methods.
options.backgroundBooleanNoIf false, redraws mounted components upon completion of the request. If true, it does not. Defaults to false.
returnsPromiseA promise that resolves to the response data, after it has been piped through the extract, deserialize and type methods
+

How to read signatures

+
+

How it works

+

The m.request utility is a thin wrapper around XMLHttpRequest, and allows making HTTP requests to remote servers in order to save and/or retrieve data from a database.

+
m.request({
+    method: "GET",
+    url: "/api/v1/users",
+})
+.then(function(users) {
+    console.log(users)
+})
+
+

A call to m.request return a promise and trigger a redraw upon completion of its promise chain.

+

By default, m.request assumes the response is in JSON format and parses it into a Javascript object (or array).

+
+

Typical usage

+

Here's an illustrative example of a component that uses m.request to retrieve some data from a server.

+
var Data = {
+    todos: {
+        list: [],
+        fetch: function() {
+            m.request({
+                method: "GET",
+                url: "/api/v1/todos",
+            })
+            .then(function(items) {
+                Data.todos.list = items
+            })
+        }
+    }
+}
+
+var Todos = {
+    oninit: Data.todos.fetch,
+    view: function(vnode) {
+        return Data.todos.list.map(function(item) {
+            return m("div", item.title)
+        })
+    }
+}
+
+m.route(document.body, "/", {
+    "/": Todos
+})
+
+

Let's assume making a request to the server URL /api/items returns an array of objects in JSON format.

+

When m.route is called at the bottom, the Todos component is initialized. oninit is called, which calls m.request. This retrieves an array of objects from the server asynchronously. "Asynchronously" means that Javascript continues running other code while it waits for the response from server. In this case, it means fetch returns, and the component is rendered using the original empty array as Data.todos.list. Once the request to the server completes, the array of objects items is assigned to Data.todos.list and the component is rendered again, yielding a list of <div>s containing the titles of each todo.

+
+

Loading icons and error messages

+

Here's an expanded version of the example above that implements a loading indicator and an error message:

+
var Data = {
+    todos: {
+        list: null,
+        error: "",
+        fetch: function() {
+            m.request({
+                method: "GET",
+                url: "/api/v1/todos",
+            })
+            .then(function(items) {
+                Data.todos.list = items
+            })
+            .catch(function(e) {
+                Data.todos.error = e.message
+            })
+        }
+    }
+}
+
+var Todos = {
+    oninit: Data.todos.fetch,
+    view: function(vnode) {
+        return Data.todos.error ? [
+            m(".error", Data.todos.error)
+        ] : Data.todos.list ? [
+            Data.todos.list.map(function(item) {
+                return m("div", item.title)
+            })
+        ] : m(".loading-icon")
+    }
+}
+
+m.route(document.body, "/", {
+    "/": Todos
+})
+
+

There are a few differences between this example and the one before. Here, Data.todos.list is null at the beginning. Also, there's an extra field error for holding an error message, and the view of the Todos component was modified to displays an error message if one exists, or display a loading icon if Data.todos.list is not an array.

+
+

Dynamic URLs

+

Request URLs may contain interpolations:

+
m.request({
+    method: "GET",
+    url: "/api/v1/users/:id",
+    data: {id: 123},
+}).then(function(user) {
+    console.log(user.id) // logs 123
+})
+
+

In the code above, :id is populated with the data from the {id: 123} object, and the request becomes GET /api/v1/users/123.

+

Interpolations are ignored if no matching data exists in the data property.

+
m.request({
+    method: "GET",
+    url: "/api/v1/users/foo:bar",
+    data: {id: 123},
+})
+
+

In the code above, the request becomes GET /api/v1/users/foo:bar

+
+

Aborting requests

+

Sometimes, it is desirable to abort a request. For example, in an autocompleter/typeahead widget, you want to ensure that only the last request completes, because typically autocompleters fire several requests as the user types and HTTP requests may complete out of order due to the unpredictable nature of networks. If another request finishes after the last fired request, the widget would display less relevant (or potentially wrong) data than if the last fired request finished last.

+

m.request() exposes its underlying XMLHttpRequest object via the options.config parameter, which allows you to save a reference to that object and call its abort method when required:

+
var searchXHR = null
+function search() {
+    abortPreviousSearch()
+
+    m.request({
+        method: "GET",
+        url: "/api/v1/users",
+        data: {search: query},
+        config: function(xhr) {searchXHR = xhr}
+    })
+}
+function abortPreviousSearch() {
+    if (searchXHR !== null) searchXHR.abort()
+    searchXHR = null
+}
+
+
+

File uploads

+

To upload files, first you need to get a reference to a File object. The easiest way to do that is from a <input type="file">.

+
m.render(document.body, [
+    m("input[type=file]", {onchange: upload})
+])
+
+function upload(e) {
+    var file = e.target.files[0]
+}
+
+

The snippet above renders a file input. If a user picks a file, the onchange event is triggered, which calls the upload function. e.target.files is a list of File objects.

+

Next, you need to create a FormData object to create a multipart request, which is a specially formatted HTTP request that is able to send file data in the request body.

+
function upload(e) {
+    var file = e.target.files[0]
+
+    var data = new FormData()
+    data.append("myfile", file)
+}
+
+

Next, you need to call m.request and set options.method to an HTTP method that uses body (e.g. POST, PUT, PATCH) and use the FormData object as options.data.

+
function upload(e) {
+    var file = e.target.files[0]
+
+    var data = new FormData()
+    data.append("myfile", file)
+
+    m.request({
+        method: "POST",
+        url: "/api/v1/upload",
+        data: data,
+    })
+}
+
+

Assuming the server is configured to accept multipart requests, the file information will be associated with the myfile key.

+

Multiple file uploads

+

It's possible to upload multiple files in one request. Doing so will make the batch upload atomic, i.e. no files will be processed if there's an error during the upload, so it's not possible to have only part of the files saved. If you want to save as many files as possible in the event of a network failure, you should consider uploading each file in a separate request instead.

+

To upload multiple files, simply append them all to the FormData object. When using a file input, you can get a list of files by adding the multiple attribute to the input:

+
m.render(document.body, [
+    m("input[type=file][multiple]", {onchange: upload})
+])
+
+function upload(e) {
+    var files = e.target.files
+
+    var data = new FormData()
+    for (var i = 0; i < files.length; i++) {
+        data.append("file" + i, file)
+    }
+
+    m.request({
+        method: "POST",
+        url: "/api/v1/upload",
+        data: data,
+    })
+}
+
+
+

Monitoring progress

+

Sometimes, if a request is inherently slow (e.g. a large file upload), it's desirable to display a progress indicator to the user to signal that the application is still working.

+

m.request() exposes its underlying XMLHttpRequest object via the options.config parameter, which allows you to attach event listeners to the XMLHttpRequest object:

+
var progress = 0
+
+m.mount(document.body, {
+    view: function() {
+        return [
+            m("input[type=file]", {onchange: upload}),
+            progress + "% completed"
+        ]
+    }
+})
+
+function upload(e) {
+    var file = e.target.files[0]
+
+    var data = new FormData()
+    data.append("myfile", file)
+
+    m.request({
+        method: "POST",
+        url: "/api/v1/upload",
+        data: data,
+        config: function(xhr) {
+            xhr.addEventListener("progress", function(e) {
+                progress = e.loaded / e.total
+
+                m.redraw() // tell Mithril that data changed and a re-render is needed
+            })
+        }
+    })
+}
+
+

In the example above, a file input is rendered. If the user picks a file, an upload is initiated, and in the config callback, a progress event handler is registered. This event handler is fired whenever there's a progress update in the XMLHttpRequest. Because the XMLHttpRequest's progress event is not directly handled by Mithril's virtual DOM engine, m.redraw() must be called to signal to Mithril that data has changed and a redraw is required.

+
+

Casting response to a type

+

Depending on the overall application architecture, it may be desirable to transform the response data of a request to a specific class or type (for example, to uniformly parse date fields or to have helper methods).

+

You can pass a constructor as the options.type parameter and Mithril will instantiate it for each object in the HTTP response.

+
function User(data) {
+    this.name = data.firstName + " " + data.lastName
+}
+
+m.request({
+    method: "GET",
+    url: "/api/v1/users",
+    type: User
+})
+.then(function(users) {
+    console.log(users[0].name) // logs a name
+})
+
+

In the example above, assuming /api/v1/users returns an array of objects, the User constructor will be instantiated (i.e. called as new User(data)) for each object in the array. If the response returned a single object, that object would be used as the data argument.

+
+

Non-JSON responses

+

Sometimes a server endpoint does not return a JSON response: for example, you may be requesting an HTML file, an SVG file, or a CSV file. By default Mithril attempts to parse a response as if it was JSON. To override that behavior, define a custom options.deserialize function:

+
m.request({
+    method: "GET",
+    url: "/files/icon.svg",
+    deserialize: function(value) {return value}
+})
+.then(function(svg) {
+    m.render(document.body, m.trust(svg))
+})
+
+

In the example above, the request retrieves an SVG file, does nothing to parse it (because deserialize merely returns the value as-is), and then renders the SVG string as trusted HTML.

+

Of course, a deserialize function may be more elaborate:

+
m.request({
+    method: "GET",
+    url: "/files/data.csv",
+    deserialize: parseCSV
+})
+.then(function(data) {
+    console.log(data)
+})
+
+function parseCSV(data) {
+    // naive implementation for the sake of keeping example simple
+    return data.split("\n").map(function(row) {
+        return row.split(",")
+    })
+}
+
+

Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays.

+

Custom headers may also be helpful in this regard. For example, if you're requesting an SVG, you probably want to set the content type accordingly. To override the default JSON request type, set options.headers to an object of key-value pairs corresponding to request header names and values.

+
m.request({
+    method: "GET",
+    url: "/files/image.svg",
+    headers: {
+        "Content-Type": "image/svg+xml; charset=utf-8",
+        "Accept": "image/svg, text/*"
+    },
+    deserialize: function(value) {return value}
+})
+
+
+

Retrieving response details

+

By default Mithril attempts to parse a response as JSON and returns xhr.responseText. It may be useful to inspect a server response in more detail, this can be accomplished by passing a custom options.extract function:

+
m.request({
+    method: "GET",
+    url: "/api/v1/users",
+    extract: function(xhr) {return {status: xhr.status, body: xhr.responseText}}
+})
+.then(function(response) {
+    console.log(response.status, response.body)
+})
+
+

The parameter to options.extract is the XMLHttpRequest object once its operation is completed, but before it has been passed to the returned promise chain, so the promise may still end up in an rejected state if processing throws an exception.

+
+

Why JSON instead of HTML

+

Many server-side frameworks provide a view engine that interpolates database data into a template before serving HTML (on page load or via AJAX) and then employ jQuery to handle user interactions.

+

By contrast, Mithril is framework designed for thick client applications, which typically download templates and data separately and combine them in the browser via Javascript. Doing the templating heavy-lifting in the browser can bring benefits like reducing operational costs by freeing server resources. Separating templates from data also allow template code to be cached more effectively and enables better code reusability across different types of clients (e.g. desktop, mobile). Another benefit is that Mithril enables a retained mode UI development paradigm, which greatly simplifies development and maintenance of complex user interactions.

+

By default, m.request expects response data to be in JSON format. In a typical Mithril application, that JSON data is then usually consumed by a view.

+

You should avoid trying to render server-generated dynamic HTML with Mithril. If you have an existing application that does use a server-side templating system, and you wish to re-architecture it, first decide whether the effort is feasible at all to begin with. Migrating from a thick server architecture to a thick client architecture is typically a somewhat large effort, and involves refactoring logic out of templates into logical data services (and the testing that goes with it).

+

Data services may be organized in many different ways depending on the nature of the application. RESTful architectures are popular with API providers, and service oriented architectures are often required where there are lots of highly transactional workflows.

+
+

Why XHR instead of fetch

+

fetch() is a newer Web API for fetching resources from servers, similar to XMLHttpRequest.

+

Mithril's m.request uses XMLHttpRequest instead of fetch() for a number of reasons:

+
    +
  • fetch is not fully standardized yet, and may be subject to specification changes.
  • +
  • XMLHttpRequest calls can be aborted before they resolve (e.g. to avoid race conditions in for instant search UIs).
  • +
  • XMLHttpRequest provides hooks for progress listeners for long running requests (e.g. file uploads).
  • +
  • XMLHttpRequest is supported by all browsers, whereas fetch() is not supported by Internet Explorer, Safari and Android (non-Chromium).
  • +
+

Currently, due to lack of browser support, fetch() typically requires a polyfill, which is over 11kb uncompressed - nearly three times larger than Mithril's XHR module.

+

Despite being much smaller, Mithril's XHR module supports many important and not-so-trivial-to-implement features like URL interpolation, querystring serialization and JSON-P requests, in addition to its ability to integrate seamlessly to Mithril's autoredrawing subsystem. The fetch polyfill does not support any of those, and requires extra libraries and boilerplates to achieve the same level of functionality.

+

In addition, Mithril's XHR module is optimized for JSON-based endpoints and makes that most common case appropriately terse - i.e. m.request(url) - whereas fetch requires an additional explicit step to parse the response data as JSON: fetch(url).then(function(response) {return response.json()})

+

The fetch() API does have a few technical advantages over XMLHttpRequest in a few uncommon cases:

+
    +
  • it provides a streaming API (in the "video streaming" sense, not in the reactive programming sense), which enables better latency and memory consumption for very large responses (at the cost of code complexity).
  • +
  • it integrates to the Service Worker API, which provides an extra layer of control over how and when network requests happen. This API also allows access to push notifications and background synchronization features.
  • +
+

In typical scenarios, streaming won't provide noticeable performance benefits because it's generally not advisable to download megabytes of data to begin with. Also, the memory gains from repeatedly reusing small buffers may be offset or nullified if they result in excessive browser repaints. For those reasons, choosing fetch() streaming instead of m.request is only recommended for extremely resource intensive applications.

+
+

Avoid anti-patterns

+

Promises are not the response data

+

The m.request method returns a Promise, not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if Javascript waited for it, it would freeze the application until the data was available.

+
// AVOID
+var users = m.request("/api/v1/users")
+console.log("list of users:", users)
+// `users` is NOT a list of users, it's a promise
+
+// PREFER
+m.request("/api/v1/users").then(function(users) {
+    console.log("list of users:", users)
+})
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

route(root, defaultRoute, routes)

+ +
+

Description

+

Navigate between "pages" within an application

+
var Home = {
+    view: function() {
+        return "Welcome"
+    }
+}
+
+m.route(document.body, "/home", {
+    "/home": Home, // defines `http://localhost/#!/home`
+})
+
+

You can only have one m.route call per application.

+
+

Signature

+

m.route(root, defaultRoute, routes)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
rootElementYesA DOM element that will be the parent node to the subtree
defaultRouteStringYesThe route to redirect to if the current URL does not match a route
routesObjectYesAn object whose keys are route strings and values are either components or a RouteResolver
returnsReturns undefined
+

How to read signatures

+

Static members

+
m.route.set
+

Redirects to a matching route, or to the default route if no matching routes can be found.

+

m.route.set(path, data, options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
pathStringYesThe path to route to, without a prefix. The path may include slots for routing parameters
dataObjectNoRouting parameters. If path has routing parameter slots, the properties of this object are interpolated into the path string
options.replaceBooleanNoWhether to create a new history entry or to replace the current one. Defaults to false
options.stateObjectNoThe state object to pass to the underlying history.pushState / history.replaceState call. This state object becomes available in the history.state property, and is merged into the routing parameters object. Note that this option only works when using the pushState API, but is ignored if the router falls back to hashchange mode (i.e. if the pushState API is not available)
options.titleStringNoThe title string to pass to the underlying history.pushState / history.replaceState call.
returnsReturns undefined
+
m.route.get
+

Returns the last fully resolved routing path, without the prefix. It may differ from the path displayed in the location bar while an asynchronous route is pending resolution.

+

path = m.route.get()

+ + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
returnsStringReturns the last fully resolved path
+
m.route.prefix
+

Defines a router prefix. The router prefix is a fragment of the URL that dictates the underlying strategy used by the router.

+

m.route.prefix(prefix)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
prefixStringYesThe prefix that controls the underlying routing strategy used by Mithril.
returnsReturns undefined
+ +

eventHandler = m.route.link(vnode)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
vnodeVnodeYesThis method is meant to be used in conjunction with an <a> vnode's oncreate hook
returnsFunction(e)Returns an event handler that calls m.route.set with the link's href as the path
+

RouteResolver

+

A RouterResolver is an object that contains an onmatch method and/or a render method. Both methods are optional, but at least one must be present. A RouteResolver is not a component, and therefore it does NOT have lifecycle methods. As a rule of thumb, RouteResolvers should be in the same file as the m.route call, whereas component definitions should be in their own modules.

+

routeResolver = {onmatch, render}

+
routeResolver.onmatch
+

The onmatch hook is called when the router needs to find a component to render. It is called once per router path changes, but not on subsequent redraws while on the same path. It can be used to run logic before a component initializes (for example authentication logic, data preloading, redirection analytics tracking, etc)

+

This method also allows you to asynchronously define what component will be rendered, making it suitable for code splitting and asynchronous module loading. To render a component asynchronously return a promise that resolves to a component.

+

For more information on onmatch, see the advanced component resolution section

+

routeResolver.onmatch(args, requestedPath)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeDescription
argsObjectThe routing parameters
requestedPathStringThe router path requested by the last routing action, including interpolated routing parameter values, but without the prefix. When onmatch is called, the resolution for this path is not complete and m.route.get() still returns the previous path.
returnsComponent|PromiseReturns a component or a promise that resolves to a component
+

If onmatch returns a component or a promise that resolves to a component, this component is used as the vnode.tag for the first argument in the RouteResolver's render method. Otherwise, vnode.tag is set to "div". Similarly, if the onmatch method is omitted, vnode.tag is also "div".

+

If onmatch returns a promise that gets rejected, the router redirects back to defaultRoute. You may override this behavior by calling .catch on the promise chain before returning it.

+
routeResolver.render
+

The render method is called on every redraw for a matching route. It is similar to the view method in components and it exists to simplify component composition.

+

vnode = routeResolve.render(vnode)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeDescription
vnodeObjectA vnode whose attributes object contains routing parameters. If onmatch does not return a component or a promise that resolves to a component, the vnode's tag field defaults to "div"
vnode.attrsObjectA map of URL parameter values
returnsVnodeReturns a vnode
+
+

How it works

+

Routing is a system that allows creating Single-Page-Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh.

+

It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism.

+

Routing without page refreshes is made partially possible by the history.pushState_method) API. Using this API, it's possible to programmatically change the URL displayed by the browser after a page has loaded, but it's the application developer's responsibility to ensure that navigating to any given URL from a cold state (e.g. a new tab) will render the appropriate markup.

+

Routing strategies

+

The routing strategy dictates how a library might actually implement routing. There are three general strategies that can be used to implement a SPA routing system, and each has different caveats:

+
    +
  • Using the fragment identifier (aka the hash) portion of the URL. A URL using this strategy typically looks like http://localhost/#!/page1
  • +
  • Using the querystring. A URL using this strategy typically looks like http://localhost/?/page1
  • +
  • Using the pathname. A URL using this strategy typically looks like http://localhost/page1
  • +
+

Using the hash strategy is guaranteed to work in browsers that don't support history.pushState (namely, Internet Explorer 9), because it can fall back to using onhashchange. Use this strategy if you want to support IE9.

+

The querystring strategy also technically works in IE9, but it falls back to reloading the page. Use this strategy if you want to support anchored links and you are not able to make the server-side necessary to support the pathname strategy.

+

The pathname strategy produces the cleanest looking URLs, but does not work in IE9 and requires setting up the server to serve the single page application code from every URL that the application can route to. Use this strategy if you want cleaner-looking URLs and do not need to support IE9.

+

Single page applications that use the hash strategy often use the convention of having an exclamation mark after the hash to indicate that they're using the hash as a routing mechanism and not for the purposes of linking to anchors. The #! string is known as a hashbang.

+

The default strategy uses the hashbang.

+
+

Typical usage

+

Normally, you need to create a few components to map routes to:

+
var Home = {
+    view: function() {
+        return [
+            m(Menu),
+            m("h1", "Home")
+        ]
+    }
+}
+
+var Page1 = {
+    view: function() {
+        return [
+            m(Menu),
+            m("h1", "Page 1")
+        ]
+    }
+}
+
+

In the example above, there are two components: Home and Page1. Each contains a menu and some text. The menu is itself being defined as a component to avoid repetition:

+
var Menu = {
+    view: function() {
+        return m("nav", [
+            m("a[href=/]", {oncreate: m.route.link}, "Home"),
+            m("a[href=/page1]", {oncreate: m.route.link}, "Page 1"),
+        ])
+    }
+}
+
+

Now we can define routes and map our components to them:

+
m.route(document.body, "/", {
+    "/": Home,
+    "/page1": Page1,
+})
+
+

Here we specify two routes: / and /page1, which render their respective components when the user navigates to each URL. By default, the SPA router prefix is #!

+
+ +

In the example above, the Menu component has two links. You can specify that their href attribute is a route URL (rather than being a regular link that navigates away from the current page), by adding the hook {oncreate: m.route.link}

+

You can also navigate programmatically, via m.route.set(route). For example, m.route.set("/page1").

+

When navigating to routes, there's no need to explicitly specify the router prefix. In other words, don't add the hashbang #! in front of the route path when linking via m.route.link or redirecting.

+
+

Routing parameters

+

Sometimes we want to have a variable id or similar data appear in a route, but we don't want to explicitly specify a separate route for every possible id. In order to achieve that, Mithril supports parameterized routes:

+
var Edit = {
+    view: function(vnode) {
+        return [
+            m(Menu),
+            m("h1", "Editing " + vnode.attrs.id)
+        ]
+    }
+}
+m.route(document.body, "/edit/1", {
+    "/edit/:id": Edit,
+})
+
+

In the example above, we defined a route /edit/:id. This creates a dynamic route that matches any URL that starts with /edit/ and is followed by some data (e.g. /edit/1, edit/234, etc). The id value is then mapped as an attribute of the component's vnode (vnode.attrs.id)

+

It's possible to have multiple arguments in a route, for example /edit/:projectID/:userID would yield the properties projectID and userID on the component's vnode attributes object.

+

In addition to routing parameters, the attrs object also includes a path property that contains the current route path, and a route property that contains the matched routed.

+

Key parameter

+

When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from /page/1 to /page/2 given a route /page/:id, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the onupdate hook, rather than oninit/oncreate. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event.

+

To achieve that, it's possible to combine route parameterization with the virtual dom key reconciliation feature:

+
m.route(document.body, "/edit/1", {
+    "/edit/:key": Edit,
+})
+
+

This means that the vnode that is created for the root component of the route has a route parameter object key. Route parameters become attrs in the vnode. Thus, when jumping from one page to another, the key changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities).

+

You can take that idea further to create components that recreate themselves when reloaded:

+

m.route.set(m.route.get(), {key: Date.now()})

+

Or even use the history state feature to achieve reloadable components without polluting the URL:

+

m.route.set(m.route.get(), null, {state: {key: Date.now()}})

+

Variadic routes

+

It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes:

+
m.route(document.body, "/edit/pictures/image.jpg", {
+    "/files/:file...": Edit,
+})
+
+

History state

+

It's possible to take full advantage of the underlying history.pushState API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form.

+

For example, you could create a form like this:

+
var state = {
+    term: "",
+    search: function() {
+        // save the state for this route
+        // this is equivalent to `history.replaceState({term: state.term}, null, location.href)`
+        m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}})
+
+        // navigate away
+        location.href = "https://google.com/?q=" + state.term
+    }
+}
+
+var Form = {
+    oninit: function(vnode) {
+        state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button
+    },
+    view: function() {
+        return m("form", [
+            m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
+            m("button", {onclick: state.search}, "Search")
+        ])
+    }
+}
+
+m.route(document.body, "/", {
+    "/": Form,
+})
+
+

This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce.

+
+

Changing router prefix

+

The router prefix is a fragment of the URL that dictates the underlying strategy used by the router.

+
// set to pathname strategy
+m.route.prefix("")
+
+// set to querystring strategy
+m.route.prefix("?")
+
+// set to hash without bang
+m.route.prefix("#")
+
+// set to pathname strategy on a non-root URL
+// e.g. if the app lives under `http://localhost/my-app` and something else lives under `http://localhost`
+m.route.prefix("/my-app")
+
+
+

Advanced component resolution

+

Instead of mapping a component to a route, you can specify a RouteResolver object. A RouteResolver object contains a onmatch() and/or a render() method. Both methods are optional but at least one of them must be present.

+
m.route(document.body, "/", {
+    "/": {
+        onmatch: function(args, requestedPath) {
+            return Home
+        },
+        render: function(vnode) {
+            return vnode // equivalent to m(Home)
+        },
+    }
+})
+
+

RouteResolvers are useful for implementing a variety of advanced routing use cases.

+
+

Wrapping a layout component

+

It's often desirable to wrap all or most of the routed components in a reusable shell (often called a "layout"). In order to do that, you first need to create a component that contains the common markup that will wrap around the various different components:

+
var Layout = {
+    view: function(vnode) {
+        return m(".layout", vnode.children)
+    }
+}
+
+

In the example above, the layout merely consists of a <div class="layout"> that contains the children passed to the component, but in a real life scenario it could be as complex as neeeded.

+

One way to wrap the layout is to define an anonymous component in the routes map:

+
// example 1
+m.route(document.body, "/", {
+    "/": {
+        view: function() {
+            return m(Layout, m(Home))
+        },
+    },
+    "/form": {
+        view: function() {
+            return m(Layout, m(Form))
+        },
+    }
+})
+
+

However, note that because the top level component is an anonymous component, jumping from the / route to the /form route (or vice-versa) will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had lifecycle methods defined, the oninit and oncreate hooks would fire on every route change. Depending on the application, this may or may not be desirable.

+

If you would prefer to have the Layout component be diffed and maintained intact rather than recreated from scratch, you should instead use a RouteResolver as the root object:

+
// example 2
+m.route(document.body, "/", {
+    "/": {
+        render: function() {
+            return m(Layout, m(Home))
+        },
+    },
+    "/form": {
+        render: function() {
+            return m(Layout, m(Form))
+        },
+    }
+})
+
+

Note that in this case, if the Layout component the oninit and oncreate lifecycle methods would only fire on the Layout component on the first route change (assuming all routes use the same layout).

+

To clarify the difference between the two examples, example 1 is equivalent to this code:

+
// functionally equivalent to example 1
+var Anon1 = {
+    view: function() {
+        return m(Layout, m(Home))
+    },
+}
+var Anon2 = {
+    view: function() {
+        return m(Layout, m(Form))
+    },
+}
+
+m.route(document.body, "/", {
+    "/": {
+        render: function() {
+            return m(Anon1)
+        }
+    },
+    "/form": {
+        render: function() {
+            return m(Anon2)
+        }
+    },
+})
+
+

Since Anon1 and Anon2 are different components, their subtrees (including Layout) are recreated from scratch. This is also what happens when components are used directly without a RouteResolver.

+

In example 2, since Layout is the top-level component in both routes, the DOM for the Layout component is diffed (i.e. left intact if it has no changes), and only the change from Home to Form triggers a recreation of that subsection of the DOM.

+
+

Authentication

+

The RouterResolver's onmatch hook can be used to run logic before the top level component in a route is initializated. The example below shows how to implement a login wall that prevents users from seeing the /secret page unless they login.

+
var isLoggedIn = false
+
+var Login = {
+    view: function() {
+        return m("form", [
+            m("button[type=button]", {
+                onclick: function() {
+                    isLoggedIn = true
+                    m.route.set("/secret")
+                }
+            }, "Login")
+        ])
+    }
+}
+
+m.route(document.body, "/secret", {
+    "/secret": {
+        onmatch: function() {
+            if (!isLoggedIn) m.route.set("/login")
+            else return Home
+        }
+    },
+    "/login": Login
+})
+
+

When the application loads, onmatch is called and since isLoggedIn is false, the application redirects to /login. Once the user pressed the login button, isLoggedIn would be set to true, and the application would redirect to /secret. The onmatch hook would run once again, and since isLoggedIn is true this time, the application would render the Home component.

+

For the sake of simplicity, in the example above, the user's logged in status is kept in a global variable, and that flag is merely toggled when the user clicks the login button. In a real life application, a user would obviously have to supply proper login credentials, and clicking the login button would trigger a request to a server to authenticate the user:

+
var Auth = {
+    username: "",
+    password: "",
+
+    setUsername: function(value) {
+        Auth.username = value
+    },
+    setPassword: function(value) {
+        Auth.password = value
+    },
+    login: function() {
+        m.request({
+            url: "/api/v1/auth",
+            data: {username: Auth.username, password: Auth.password}
+        }).then(function(data) {
+            localStorage.setItem("auth-token": data.token)
+            m.route.set("/secret")
+        })
+    }
+}
+
+var Login = {
+    view: function() {
+        return m("form", [
+            m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
+            m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
+            m("button[type=button]", {onclick: Auth.login, "Login")
+        ])
+    }
+}
+
+m.route(document.body, "/secret", {
+    "/secret": {
+        onmatch: function() {
+            if (!localStorage.getItem("auth-token")) m.route.set("/login")
+            else return Home
+        }
+    },
+    "/login": Login
+})
+
+
+

Preloading data

+

Typically, a component can load data upon initialization. Loading data this way renders the component twice (once upon routing, and once after the request completes).

+
var state = {
+    users: [],
+    loadUsers: function() {
+        return m.request("/api/v1/users").then(function(users) {
+            state.users = users
+        })
+    }
+}
+
+m.route(document.body, "/user/list", {
+    "/user/list": {
+        oninit: state.loadUsers,
+        view: function() {
+            return state.users.length > 0 ? state.users.map(function(user) {
+                return m("div", user.id)
+            }) : "loading"
+        }
+    },
+})
+
+

In the example above, on the first render, the UI displays "loading" since state.users is an empty array before the request completes. Then, once data is available, the UI redraws and a list of user ids is shown.

+

RouteResolvers can be used as a mechanism to preload data before rendering a component in order to avoid UI flickering and thus bypassing the need for a loading indicator:

+
var state = {
+    users: [],
+    loadUsers: function() {
+        return m.request("/api/v1/users").then(function(users) {
+            state.users = users
+        })
+    }
+}
+
+m.route(document.body, "/user/list", {
+    "/user/list": {
+        onmatch: state.loadUsers,
+        render: function() {
+            return state.users.map(function(user) {
+                return m("div", user.id)
+            })
+        }
+    },
+})
+
+

Above, render only runs after the request completes, making the ternary operator redundant.

+
+

Code splitting

+

In a large application, it may be desirable to download the code for each route on demand, rather than upfront. Dividing the codebase this way is known as code splitting or lazy loading. In Mithril, this can be accomplished by returning a promise from the onmatch hook:

+

At its most basic form, one could do the following:

+
// Home.js
+module.export = {
+    view: function() {
+        return [
+            m(Menu),
+            m("h1", "Home")
+        ]
+    }
+}
+
+
// index.js
+function load(file) {
+    return m.request({
+        method: "GET",
+        url: file,
+        extract: function(xhr) {
+            return new Function("var module = {};" + xhr.responseText + ";return module.exports;")
+        }
+    })
+}
+
+m.route(document.body, "/", {
+    "/": {
+        onmatch: function() {
+            return load("Home.js")
+        },
+    },
+})
+
+

However, realistically, in order for that to work on a production scale, it would be necessary to bundle all of the dependencies for the Home.js module into the file that is ultimately served by the server.

+

Fortunately, there are a number of tools that facilitate the task of bundling modules for lazy loading. Here's an example using webpack's code splitting system:

+
m.route(document.body, "/", {
+    "/": {
+        onmatch: function() {
+            // using Webpack async code splitting
+            return new Promise(function(resolve) {
+                require(['./Home.js'], resolve)
+            })
+        },
+    },
+})
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

How to read signatures

+ +

Signature sections typically look like this:

+

vnode = m(selector, attributes, children)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
selectorString|ObjectYesA CSS selector or a component
attributesObjectNoHTML attributes or element properties
childrenArray<Vnode>|String|Number|BooleanNoChild vnodes. Can be written as splat arguments
returnsVnodeA vnode
+

The signature line above the table indicates the general syntax of the method, showing the name of the method, the order of its arguments and a suggested variable name for its return value.

+

The Argument column in the table indicates which part of the signature is explained by the respective table row. The returns row displays information about the return value of the method.

+

The Type column indicates the expected type for the argument.

+

A pipe (|) indicates that an argument is valid if it has any of the listed types. For example, String|Object indicates that selector can be a string OR an object.

+

Angled brackets (< >) after an Array indicate the expected type for array items. For exampe, Array<String> indicates that the argument must be an array and that all items in that array must be strings. Angled brackets after an Object indicate a map. For example, Object<String,Component> indicates that the argument must be an object, whose keys are strings and values are components

+

Sometimes non-native types may appear to indicate that a specific object signature is required. For example, Vnode is an object that has a virtual DOM node structure.

+

The Required column indicates whether an argument is required or optional. If an argument is optional, you may set it to null or undefined, or omit it altogether, such that the next argument appears in its place.

+
+

Optional arguments

+

Function arguments surrounded by square brackets [ ] are optional. In the example below, url is an optional argument:

+

m.request([url,] options)

+
+

Splats

+

A splat argument means that if the argument is an array, you can omit the square brackets and have a variable number of arguments in the method instead.

+

In the example at the top, this means that m("div", {id: "foo"}, ["a", "b", "c"]) can also be written as m("div", {id: "foo"}, "a", "b", "c").

+

Splats are useful in some compile-to-js languages such as Coffeescript, and also allow helpful shorthands for some common use cases.

+
+

Function signatures

+

Functions are denoted with an arrow (->). The left side of the arrow indicates the types of the input arguments and the right side indicates the type for the return value.

+

For example, parseFloat has the signature String -> Number, i.e. it takes a string as input and returns a number as output.

+

Functions with multiple arguments are denoted with parenthesis: (String, Array) -> Number

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Simple application

+ +

Let's develop a simple application that covers some of the major aspects of Single Page Applications

+

First let's create an entry point for the application. Create a file index.html:

+
<!doctype html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <title>My Application</title>
+    </head>
+    <body>
+        <script src="bin/app.js"></script>
+    </body>
+</html>
+
+

The <!doctype html> line indicates this is an HTML 5 document. The first charset meta tag indicates the encoding of the document and the viewport meta tag dictates how mobile browsers should scale the page. The title tag contains the text to be displayed on the browser tab for this application, and the script tag indicates what is the path to the Javascript file that controls the application.

+

We could create the entire application in a single Javascript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into modules, and assemble these modules into a bundle bin/app.js.

+

There are many ways to setup a bundler tool, but most are distributed via NPM. In fact, most modern Javascript libraries and tools are distributed that way, including Mithril. NPM stands for Node.js Package Manager. To download NPM, install Node.js; NPM is installed automatically with it. Once you have Node.js and NPM installed, open the command line and run this command:

+
npm init -y
+
+

If NPM is installed correctly, a file package.json will be created. This file will contain a skeleton project meta-description file. Feel free to edit the project and author information in this file.

+
+

To install Mithril, follow the instructions in the installation page. Once you have a project skeleton with Mithril installed, we are ready to create the application.

+

Let's start by creating a module to store our state. Let's create a file called src/models/User.js

+
// src/models/User.js
+var User = {
+    list: []
+}
+
+module.exports = User
+
+

Now let's add code to load some data from a server. To communicate with a server, we can use Mithril's XHR utility, m.request. First, we include Mithril in the module:

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: []
+}
+
+module.exports = User
+
+

Next we create a function that will trigger an XHR call. Let's call it loadList

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: [],
+    loadList: function() {
+        // TODO: make XHR call
+    }
+}
+
+module.exports = User
+
+

Then we can add an m.request call to make an XHR request. For this tutorial, we'll make XHR calls to the REM API, a mock REST API designed for rapid prototyping. This API returns a list of users from the GET http://rem-rest-api.herokuapp.com/api/users endpoint. Let's use m.request to make an XHR request and populate our data with the response of that endpoint.

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: [],
+    loadList: function() {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users",
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.list = result.data
+        })
+    },
+}
+
+module.exports = User
+
+

The method option is an HTTP method. To retrieve data from the server without causing side-effects on the server, we need to use the GET method. The url is the address for the API endpoint. The withCredentials: true line indicates that we're using cookies (which is a requirement for the REM API).

+

The m.request call returns a Promise that resolves to the data from the endpoint. By default, Mithril assumes a HTTP response body are in JSON format and automatically parses it into a Javascript object or array. The .then callback runs when the XHR request completes. In this case, the callback assigns the result.data array to User.list.

+

Notice we also have a return statement in loadList. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request.

+

This simple model exposes two members: User.list (an array of user objects), and User.loadList (a method that populates User.list with server data).

+
+

Now, let's create a view module so that we can display data from our User model module.

+

Create a file called src/views/UserList.js. First, let's include Mithril and our model, since we'll need to use both:

+
// src/views/UserList.js
+var m = require("mithril")
+var User = require("../models/User")
+
+

Next, let's create a Mithril component. A component is simply an object that has a view method:

+
// src/views/UserList.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    view: function() {
+        // TODO add code here
+    }
+}
+
+

By default, Mithril views are described using hyperscript. Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and in addition, since its syntax is simply Javascript, it's possible to leverage a lot of Javascript tooling ecosystem: for example Babel, JSX (inline-HTML syntax extension), eslint (linting), uglifyjs (minification), istanbul (code coverage), flow (static type analysis), etc.

+

Let's use Mithril hyperscript to create a list of items. Hyperscript is the most idiomatic way of writing Mithril views, but JSX is another popular alternative that you could explore once you're more comfortable with the basics:

+
var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    view: function() {
+        return m(".user-list")
+    }
+}
+
+

The ".user-list" string is a CSS selector, and as you would expect, .user-list represents a class. When a tag is not specified, div is the default. So this view is equivalent to <div class="user-list"></div>.

+

Now, let's reference the list of users from the model we created earlier (User.list) to dynamically loop through data:

+
// src/views/UserList.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    view: function() {
+        return m(".user-list", User.list.map(function(user) {
+            return m(".user-list-item", user.firstName + " " + user.lastName)
+        }))
+    }
+}
+
+

Since User.list is a Javascript array, and since hyperscript views are just Javascript, we can loop through the array using the .map method. This creates an array of vnodes that represents a list of divs, each containing the name of a user.

+

The problem, of course, is that we never called the User.loadList function. Therefore, User.list is still an empty array, and thus this view would render a blank page. Since we want User.loadList to be called when we render this component, we can take advantage of component lifecycle methods:

+
// src/views/UserList.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    oninit: User.loadList,
+    view: function() {
+        return m(".user-list", User.list.map(function(user) {
+            return m(".user-list-item", user.firstName + " " + user.lastName)
+        }))
+    }
+}
+
+

Notice that we added an oninit method to the component, which references User.loadList. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, User.list gets populated.

+

Also notice we didn't do oninit: User.loadList() (with parentheses at the end). The difference is that oninit: User.loadList() calls the function once and immediately, but oninit: User.loadList only calls that function when the component renders. This is an important difference and a common pitfall for developers new to javascript: calling the function immediately means that the XHR request will fire as soon as the source code is evaluated, even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected.

+
+

Let's render the view from the entry point file src/index.js we created earlier:

+
// src/index.js
+var m = require("mithril")
+
+var UserList = require("./views/UserList")
+
+m.mount(document.body, UserList)
+
+

The m.mount call renders the specified component (UserList) into a DOM element (document.body), erasing any DOM that was there previously. Opening the HTML file in a browser should now display a list of person names.

+
+

Right now, the list looks rather plain because we have not defined any styles.

+

There are many similar conventions and libraries that help organize application styles nowadays. Some, like Bootstrap dictate a specific set of HTML structures and semantically meaningful class names, which has the upside of providing low cognitive dissonance, but the downside of making customization more difficult. Others, like Tachyons provide a large number of self-describing, atomic class names at the cost of making the class names themselves non-semantic. "CSS-in-JS" is another type of CSS system that is growing in popularity, which basically consists of scoping CSS via transpilation tooling. CSS-in-JS libraries achieve maintainability by reducing the size of the problem space, but come at the cost of having high complexity.

+

Regardless of what CSS convention/library you choose, a good rule of thumb is to avoid the cascading aspect of CSS. To keep this tutorial simple, we'll just use plain CSS with overly explicit class names, so that the styles themselves provide the atomicity of Tachyons, and class name collisions are made unlikely through the verbosity of the class names. Plain CSS can be sufficient for low-complexity projects (e.g. 3 to 6 man-months of initial implementation time and few project phases).

+

To add styles, let's first create a file called styles.css and include it in the index.html file:

+
<!doctype html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <title>My Application</title>
+        <link href="styles.css" rel="stylesheet" />
+    </head>
+    <body>
+        <script src="bin/app.js"></script>
+    </body>
+</html>
+
+

Now we can style the UserList component:

+
.user-list {list-style:none;margin:0 0 10px;padding:0;}
+.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
+.user-list-item:hover {text-decoration:underline;}
+
+

The CSS above is written using a convention of keeping all styles for a rule in a single line, in alphabetical order. This convention is designed to take maximum advantage of screen real estate, and makes it easier to scan the CSS selectors (since they are always on the left side) and their logical grouping, and it enforces predictable and uniform placement of CSS rules for each selector.

+

Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviate from the more widespread cosmetic-oriented spacing conventions.

+

Reloading the browser window now should display some styled elements.

+
+

Let's add routing to our application.

+

Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via Javascript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML).

+

We can add routing by changing the m.mount call to a m.route call:

+
// src/index.js
+var m = require("mithril")
+
+var UserList = require("./views/UserList")
+
+m.route(document.body, "/list", {
+    "/list": UserList
+})
+
+

The m.route call specifies that the application will be rendered into document.body. The "/list" argument is the default route. That means the user will be redirected to that route if they land in a route that does not exist. The {"/list": UserList} object declares a map of existing routes, and what components each route resolves to.

+

Refreshing the page in the browser should now append #!/list to the URL to indicate that routing is working. Since that route render UserList, we should still see the list of people on screen as before.

+

The #! snippet is known as a hashbang, and it's a commonly used string for implementing client-side routing. It's possible to configure this string it via m.route.prefix. Some configurations require supporting server-side changes, so we'll just continue using the hashbang for the rest of this tutorial.

+
+

Let's add another route to our application for editing users. First let's create a module called views/UserForm.js

+
// src/views/UserForm.js
+
+module.exports = {
+    view: function() {
+        // TODO implement view
+    }
+}
+
+

Then we can require this new module from src/index.js

+
// src/index.js
+var m = require("mithril")
+
+var UserList = require("./views/UserList")
+var UserForm = require("./views/UserForm")
+
+m.route(document.body, "/list", {
+    "/list": UserList
+})
+
+

And finally, we can create a route that references it:

+
// src/index.js
+var m = require("mithril")
+
+var UserList = require("./views/UserList")
+var UserForm = require("./views/UserForm")
+
+m.route(document.body, "/list", {
+    "/list": UserList,
+    "/edit/:id": UserForm,
+})
+
+

Notice that the new route has a :id in it. This is a route parameter; you can think of it as a wild card; the route /edit/1 would resolve to UserForm with an id of "1". /edit/2 would also resolve to UserForm, but with an id of "2". And so on.

+

Let's implement the UserForm component so that it can respond to those route parameters:

+
// src/views/UserForm.js
+var m = require("mithril")
+
+module.exports = {
+    view: function() {
+        return m("form", [
+            m("label.label", "First name"),
+            m("input.input[type=text][placeholder=First name]"),
+            m("label.label", "Last name"),
+            m("input.input[placeholder=Last name]"),
+            m("button.button[type=submit]", "Save"),
+        ])
+    }
+}
+
+

And let's add some styles to styles.css:

+
/* styles.css */
+body,.input,.button {font:normal 16px Verdana;margin:0;}
+
+.user-list {list-style:none;margin:0 0 10px;padding:0;}
+.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
+.user-list-item:hover {text-decoration:underline;}
+
+.label {display:block;margin:0 0 5px;}
+.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
+.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
+.button:hover {background:#e8e8e8;}
+
+

Right now, this component does nothing to respond to user events. Let's add some code to our User model in src/models/User.js. This is how the code is right now:

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: [],
+    loadList: function() {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users",
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.list = result.data
+        })
+    },
+}
+
+module.exports = User
+
+

Let's add code to allow us to load a single user

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: [],
+    loadList: function() {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users",
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.list = result.data
+        })
+    },
+
+    current: {},
+    load: function(id) {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users/:id",
+            data: {id: id},
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.current = result
+        })
+    }
+}
+
+module.exports = User
+
+

Notice we added a User.current property, and a User.load(id) method which populates that property. We can now populate the UserForm view using this new method:

+
// src/views/UserForm.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    oninit: function(vnode) {User.load(vnode.attrs.id)},
+    view: function() {
+        return m("form", [
+            m("label.label", "First name"),
+            m("input.input[type=text][placeholder=First name]", {value: User.current.firstName}),
+            m("label.label", "Last name"),
+            m("input.input[placeholder=Last name]", {value: User.current.lastName}),
+            m("button.button[type=submit]", "Save"),
+        ])
+    }
+}
+
+

Similar to the UserList component, oninit calls User.load(). Remember we had a route parameter called :id on the "/edit/:id": UserForm route? The route parameter becomes an attribute of the UserForm component's vnode, so routing to /edit/1 would make vnode.attrs.id have a value of "1".

+

Now, let's modify the UserList view so that we can navigate from there to a UserForm:

+
// src/views/UserList.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    oninit: User.loadList,
+    view: function() {
+        return m(".user-list", User.list.map(function(user) {
+            return m("a.user-list-item", {href: "/edit/" + user.id, oncreate: m.route.link}, user.firstName + " " + user.lastName)
+        }))
+    }
+}
+
+

Here we changed .user-list-item to a.user-list-item. We added an href that references the route we want, and finally we added oncreate: m.route.link. This makes the link behave like a routed link (as opposed to merely behaving like a regular link). What this means is that clicking the link would change the part of URL that comes after the hashbang #! (thus changing the route without unloading the current HTML page)

+

If you refresh the page in the browser, you should now be able to click on a person and be taken to a form. You should also be able to press the back button in the browser to go back from the form to the list of people.

+
+

The form itself still doesn't save when you press "Save". Let's make this form work:

+
// src/views/UserForm.js
+var m = require("mithril")
+var User = require("../models/User")
+
+module.exports = {
+    oninit: function(vnode) {User.load(vnode.attrs.id)},
+    view: function() {
+        return m("form", [
+            m("label.label", "First name"),
+            m("input.input[type=text][placeholder=First name]", {
+                oninput: m.withAttr("value", function(value) {User.current.firstName = value}),
+                value: User.current.firstName
+            }),
+            m("label.label", "Last name"),
+            m("input.input[placeholder=Last name]", {
+                oninput: m.withAttr("value", function(value) {User.current.lastName = value}),
+                value: User.current.lastName
+            }),
+            m("button.button[type=submit]", {onclick: User.save}, "Save"),
+        ])
+    }
+}
+
+

We added oninput events to both inputs, that set the User.current.firstName and User.current.lastName properties when a user types.

+

In addition, we declared that a User.save method should be called when the "Save" button is pressed. Let's implement that method:

+
// src/models/User.js
+var m = require("mithril")
+
+var User = {
+    list: [],
+    loadList: function() {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users",
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.list = result.data
+        })
+    },
+
+    current: {},
+    load: function(id) {
+        return m.request({
+            method: "GET",
+            url: "http://rem-rest-api.herokuapp.com/api/users/:id",
+            data: {id: id},
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.current = result
+        })
+    },
+
+    save: function() {
+        return m.request({
+            method: "PUT",
+            url: "http://rem-rest-api.herokuapp.com/api/users/:id",
+            data: User.current,
+            withCredentials: true,
+        })
+    }
+}
+
+module.exports = User
+
+

In the save method at the bottom, we used the PUT HTTP method to indicate that we are upserting data to the server.

+

Now try editing the name of a user in the application. Once you save a change, you should be able to see the change reflected in the list of users.

+
+

Currently, we're only able to navigate back to the user list via the browser back button. Ideally, we would like to have a menu - or more generically, a layout where we can put global UI elements

+

Let's create a file src/views/Layout.js:

+
var m = require("mithril")
+
+module.exports = {
+    view: function(vnode) {
+        return m("main.layout", [
+            m("nav.menu", [
+                m("a[href='/list']", {oncreate: m.route.link}, "Users")
+            ]),
+            m("section", vnode.children)
+        ])
+    }
+}
+
+

This component is fairly straightforward, it has a <nav> with a link to the list of users. Similar to what we did to the /edit links, this link uses m.route.link to activate routing behavior in the link.

+

Notice there's also a <section> element with vnode.children as children. vnode is a reference to the vnode that represents an instance of the Layout component (i.e. the vnode returned by a m(Layout) call). Therefore, vnode.children refer to any children of that vnode.

+

Let's add some styles:

+
/* styles.css */
+body,.input,.button {font:normal 16px Verdana;margin:0;}
+
+.layout {margin:10px auto;max-width:1000px;}
+.menu {margin:0 0 30px;}
+
+.user-list {list-style:none;margin:0 0 10px;padding:0;}
+.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;}
+.user-list-item:hover {text-decoration:underline;}
+
+.label {display:block;margin:0 0 5px;}
+.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;}
+.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;}
+.button:hover {background:#e8e8e8;}
+
+

Let's change the router in src/index.js to add our layout into the mix:

+
// src/index.js
+var m = require("mithril")
+
+var UserList = require("./views/UserList")
+var UserForm = require("./views/UserForm")
+var Layout = require("./views/Layout")
+
+m.route(document.body, "/list", {
+    "/list": {
+        render: function() {
+            return m(Layout, m(UserList))
+        }
+    },
+    "/edit/:id": {
+        render: function(vnode) {
+            return m(Layout, m(UserForm, vnode.attrs))
+        }
+    },
+})
+
+

We replaced each component with a RouteResolver (basically, an object with a render method). The render methods can be written in the same way as regular component views would be, by nesting m() calls.

+

The interesting thing to pay attention to is how components can be used instead of a selector string in a m() call. Here, in the /list route, we have m(Layout, m(UserList)). This means there's a root vnode that represents an instance of Layout, which has a UserList vnode as its only child.

+

In the /edit/:id route, there's also a vnode argument that carries the route parameters into the UserForm component. So if the URL is /edit/1, then vnode.attrs in this case is {id: 1}, and this m(UserForm, vnode.attrs) is equivalent to m(UserForm, {id: 1}). The equivalent JSX code would be <UserForm id={vnode.attrs} />.

+

Refresh the page in the browser and now you'll see the global navigation on every page in the app.

+
+

This concludes the tutorial.

+

In this tutorial, we went through the process of creating a very simple application where we can list users from a server and edit them individually. As an extra exercise, try to implement user creation and deletion on your own.

+

If you want to see more examples of Mithril code, check the examples page. If you have questions, feel free to drop by the Mithril chat room.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

stream()

+ +
+

Description

+

A Stream is a reactive data structure, similar to cells in spreadsheet applications.

+

For example, in a spreadsheet, if A1 = B1 + C1, then changing the value of B1 or C1 automatically changes the value of A1.

+

Similarly, you can make a stream depend on other streams so that changing the value of one automatically updates the other. This is useful when you have very expensive computations and want to only run them when necessary, as opposed to, say, on every redraw.

+

Streams are NOT bundled with Mithril's core distribution. To include the Streams module, use:

+
var Stream = require("mithril/stream")
+
+
+

Signature

+

Creates a stream

+

stream = Stream(value)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
valueanyNoIf this argument is present, the value of the stream is set to it
returnsStreamReturns a stream
+

How to read signatures

+
+

Static members

+
Stream.combine
+

Creates a computed stream that reactively updates if any of its upstreams are updated. See combining streams

+

stream = Stream.combine(combiner, streams)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
combiner(Stream..., Array) -> anyYesSee combiner argument
streamsArray<Stream>YesA list of streams to be combined
returnsStreamReturns a stream
+

How to read signatures

+
+
combiner
+

Specifies how the value of a computed stream is generated. See combining streams

+

any = combiner(streams..., changed)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
streams...splat of StreamsNoSplat of zero or more streams that correspond to the streams passed as the second argument to stream.combine
changedArray<Stream>YesList of streams that were affected by an update
returnsanyReturns a computed value
+

How to read signatures

+
+
Stream.merge
+

Creates a stream whose value is the array of values from an array of streams

+

stream = Stream.merge(streams)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
streamsArray<Stream>YesA list of streams
returnsStreamReturns a stream whose value is an array of input stream values
+

How to read signatures

+
+
Stream.HALT
+

A special value that can be returned to stream callbacks to halt execution of downstreams

+
+
Stream["fantasy-land/of"]
+

This method is functionally identical to stream. It exists to conform to Fantasy Land's Applicative specification. For more information, see the What is Fantasy Land section.

+

stream = stream["fantasy-land/of"](value)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
valueanyNoIf this argument is present, the value of the stream is set to it
returnsStreamReturns a stream
+
+

Instance members

+
stream.map
+

Creates a dependent stream whose value is set to the result of the callback function. This method is an alias of stream["fantasy-land/map"].

+

dependentStream = stream().map(callback)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
callbackany -> anyYesA callback whose return value becomes the value of the stream
returnsStreamReturns a stream
+

How to read signatures

+
+
stream.end
+

A co-dependent stream that unregisters dependent streams when set to true. See ended state.

+

endStream = stream().end

+
+
stream["fantasy-land/of"]
+

This method is functionally identical to stream. It exists to conform to Fantasy Land's Applicative specification. For more information, see the What is Fantasy Land section.

+

stream = stream()["fantasy-land/of"](value)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
valueanyNoIf this argument is present, the value of the stream is set to it
returnsStreamReturns a stream
+
+
stream["fantasy-land/map"]
+

Creates a dependent stream whose value is set to the result of the callback function. See chaining streams

+

This method exists to conform to Fantasy Land's Applicative specification. For more information, see the What is Fantasy Land section.

+

dependentStream = stream()["fantasy-land/of"](callback)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
callbackany -> anyYesA callback whose return value becomes the value of the stream
returnsStreamReturns a stream
+

How to read signatures

+
+
stream["fantasy-land/ap"]
+

The name of this method stands for apply. If a stream a has a function as its value, another stream b can use it as the argument to b.ap(a). Calling ap will call the function with the value of stream b as its argument, and it will return another stream whose value is the result of the function call. This method exists to conform to Fantasy Land's Applicative specification. For more information, see the What is Fantasy Land section.

+

stream = stream()["fantasy-land/ap"](apply)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
applyStreamYesA stream whose value is a function
returnsStreamReturns a stream
+
+

Basic usage

+

Streams are not part of the core Mithril distribution. To include them in a project, require its module:

+
var stream = require("mithril/stream")
+
+

Streams as variables

+

stream() returns a stream. At its most basic level, a stream works similar to a variable or a getter-setter property: it can hold state, which can be modified.

+
var username = stream("John")
+console.log(username()) // logs "John"
+
+username("John Doe")
+console.log(username()) // logs "John Doe"
+
+

The main difference is that a stream is a function, and therefore can be composed into higher order functions.

+
var users = stream()
+
+// request users from a server using the fetch API
+fetch("/api/users")
+    .then(function(response) {return response.json()})
+    .then(users)
+
+

In the example above, the users stream is populated with the response data when the request resolves.

+

Bidirectional bindings

+

Streams can also be populated from other higher order functions, such as m.withAttr

+
// a stream
+var user = stream("")
+
+// a bi-directional binding to the stream
+m("input", {
+    oninput: m.withAttr("value", user),
+    value: user()
+})
+
+

In the example above, when the user types in the input, the user stream is updated to the value of the input field.

+

Computed properties

+

Streams are useful for implementing computed properties:

+
var title = stream("")
+var slug = title.map(function(value) {
+    return value.toLowerCase().replace(/\W/g, "-")
+})
+
+title("Hello world")
+console.log(slug()) // logs "hello-world"
+
+

In the example above, the value of slug is computed when title is updated, not when slug is read.

+

It's of course also possible to compute properties based on multiple streams:

+
var firstName = stream("John")
+var lastName = stream("Doe")
+var fullName = stream.merge([firstName, lastName]).map(function(values) {
+    return values.join(" ")
+})
+
+console.log(fullName()) // logs "John Doe"
+
+firstName("Mary")
+
+console.log(fullName()) // logs "Mary Doe"
+
+

Computed properties in Mithril are updated atomically: streams that depend on multiple streams will never be called more than once per value update, no matter how complex the computed property's dependency graph is.

+
+

Chaining streams

+

Streams can be chained using the map method. A chained stream is also known as a dependent stream.

+
// parent stream
+var value = stream(1)
+
+// dependent stream
+var doubled = value.map(function(value) {
+    return value * 2
+})
+
+console.log(doubled()) // logs 2
+
+

Dependent streams are reactive: their values are updated any time the value of their parent stream is updated. This happens regardless of whether the dependent stream was created before or after the value of the parent stream was set.

+

You can prevent dependent streams from being updated by returning the special value stream.HALT

+
var halted = stream(1).map(function(value) {
+    return stream.HALT
+})
+
+halted.map(function() {
+    // never runs
+})
+
+
+

Combining streams

+

Streams can depend on more than one parent stream. These kinds of streams can be created via stream.merge()

+
var a = stream("hello")
+var b = stream("world")
+
+var greeting = stream.merge([a, b]).map(function(values) {
+    return values.join(" ")
+})
+
+console.log(greeting()) // logs "hello world"
+
+

There's also a lower level method called stream.combine() that exposes the stream themselves in the reactive computations for more advanced use cases

+
var a = stream(5)
+var b = stream(7)
+
+var added = stream.combine(function(a, b) {
+    return a() + b()
+}, [a, b])
+
+console.log(added()) // logs 12
+
+

A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also bring the performance benefits of not recomputing downstreams unnecessarily.

+

You can prevent dependent streams from being updated by returning the special value stream.HALT

+
var halted = stream.combine(function(stream) {
+    return stream.HALT
+}, [stream(1)])
+
+halted.map(function() {
+    // never runs
+})
+
+
+

Stream states

+

At any given time, a stream can be in one of three states: pending, active, and ended.

+

Pending state

+

Pending streams can be created by calling stream() with no parameters.

+
var pending = stream()
+
+

If a stream is dependent on more than one stream, any of its parent streams is in a pending state, the dependent streams is also in a pending state, and does not update its value.

+
var a = stream(5)
+var b = stream() // pending stream
+
+var added = stream.combine(function(a, b) {
+    return a() + b()
+}, [a, b])
+
+console.log(added()) // logs undefined
+
+

In the example above, added is a pending stream, because its parent b is also pending.

+

This also applies to dependent streams created via stream.map:

+
var value = stream()
+var doubled = value.map(function(value) {return value * 2})
+
+console.log(doubled()) // logs undefined because `doubled` is pending
+
+

Active state

+

When a stream receives a value, it becomes active (unless the stream is ended).

+
var stream1 = stream("hello") // stream1 is active
+
+var stream2 = stream() // stream2 starts off pending
+stream2("world") // then becomes active
+
+

A dependent stream with multiple parents becomes active if all of its parents are active.

+
var a = stream("hello")
+var b = stream()
+
+var greeting = stream.merge([a, b]).map(function(values) {
+    return values.join(" ")
+})
+
+

In the example above, the a stream is active, but b is pending. setting b("world") would cause b to become active, and therefore greeting would also become active, and be updated to have the value "hello world"

+

Ended state

+

A stream can stop affecting its dependent streams by calling stream.end(true). This effectively removes the connection between a stream and its dependent streams.

+
var value = stream()
+var doubled = value.map(function(value) {return value * 2})
+
+value.end(true) // set to ended state
+
+value(5)
+
+console.log(doubled())
+// logs undefined because `doubled` no longer depends on `value`
+
+

Ended streams still have state container semantics, i.e. you can still use them as getter-setters, even after they are ended.

+
var value = stream(1)
+value.end(true) // set to ended state
+
+console.log(value(1)) // logs 1
+
+value(2)
+console.log(value()) // logs 2
+
+

Ending a stream can be useful in cases where a stream has a limited lifetime (for example, reacting to mousemove events only while a DOM element is being dragged, but not after it's been dropped).

+
+

Serializing streams

+

Streams implement a .toJSON() method. When a stream is passed as the argument to JSON.stringify(), the value of the stream is serialized.

+
var value = stream(123)
+var serialized = JSON.stringify(value)
+console.log(serialized) // logs 123
+
+

Streams also implement a valueOf method that returns the value of the stream.

+
var value = stream(123)
+console.log("test " + value) // logs "test 123"
+
+
+

Streams do not trigger rendering

+

Unlike libraries like Knockout, Mithril streams do not trigger re-rendering of templates. Redrawing happens in response to event handlers defined in Mithril component views, route changes, or after m.request calls resolve.

+

If redrawing is desired in response to other asynchronous events (e.g. setTimeout/setInterval, websocket subscription, 3rd party library event handler, etc), you should manually call m.redraw()

+
+

What is Fantasy Land

+

Fantasy Land specifies interoperability of common algebraic structures. In plain english, that means that libraries that conform to Fantasy Land specs can be used to write generic functional style code that works regardless of how these libraries implement the constructs.

+

For example, say we want to create a generic function called plusOne. The naive implementation would look like this:

+
function plusOne(a) {
+    return a + 1
+}
+
+

The problem with this implementation is that it can only be used with a number. However it's possible that whatever logic produces a value for a could also produce an error state (wrapped in a Maybe or an Either from a library like Sanctuary or Ramda-Fantasy), or it could be a Mithril stream, or a flyd stream, etc. Ideally, we wouldn't want to write a similar version of the same function for every possible type that a could have and we wouldn't want to be writing wrapping/unwrapping/error handling code repeatedly.

+

This is where Fantasy Land can help. Let's rewrite that function in terms of a Fantasy Land algebra:

+
var fl = require("fantasy-land")
+
+function plusOne(a) {
+    return a[fl.map](function(value) {return value + 1})
+}
+
+

Now this method works with any Fantasy Land compliant Functor, such as R.Maybe, S.Either, stream, etc.

+

This example may seem convoluted, but it's a trade-off in complexity: the naive plusOne implementation makes sense if you have a simple system and only ever increment numbers, but the Fantasy Land implementation becomes more powerful if you have a large system with many wrapper abstractions and reused algorithms.

+

When deciding whether you should adopt Fantasy Land, you should consider your team's familiarity with functional programming, and be realistic regarding the level of discipline that your team can commit to maintaining code quality (vs the pressure of writing new features and meeting deadlines). Functional style programming heavily depends on compiling, curating and mastering a large set of small, precisely defined functions, and therefore it's not suitable for teams who do not have solid documentation practices, and/or lack experience in functional oriented languages.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Testing

+ +

Mithril comes with a testing framework called ospec. What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding yak shaving and analysis paralysis.

+

The easist way to setup the test runner is to create an NPM script for it. Open your project's package.json file and edit the test line under the scripts section:

+
{
+    "name": "my-project",
+    "scripts": {
+        "test": "ospec"
+    }
+}
+

Remember this is a JSON file, so object key names such as "test" must be inside of double quotes.

+

To setup a test suite, create a tests folder and inside of it, create a test file:

+
// file: tests/math-test.js
+var o = require("mithril/ospec/ospec")
+
+o.spec("math", function() {
+    o("addition works", function() {
+        o(1 + 2).equals(3)
+    })
+})
+
+

To run the test, use the command npm test. Ospec considers any Javascript file inside of a tests folder (anywhere in the project) to be a test.

+
npm test
+

+

Good testing practices

+

Generally speaking, there are two ways to write tests: upfront and after the fact.

+

Writing tests upfront requires specifications to be frozen. Upfront tests are a great way of codifying the rules that a yet-to-be-implemented API must obey. However, writing tests upfront may not be a suitable strategy if you don't have a reasonable idea of what your project will look like, if the scope of the API is not well known or if it's likely to change (e.g. based on previous history at the company).

+

Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertedly broken and that previously fixed bugs do not get re-introduced by unrelated changes.

+
+

Unit testing

+

Unit testing is the practice of isolating a part of an application (typically a single module), and asserting that, given some inputs, it produces the expected outputs.

+

Testing a Mithril component is easy. Let's assume we have a simple component like this:

+
// MyComponent.js
+var m = require("mithril")
+
+module.exports = {
+    view: function() {
+        return m("div", "Hello world")
+    }
+}
+
+

We can then create a tests/MyComponent.js file and create a test for this component like this:

+
var MyComponent = require("MyComponent")
+
+o.spec("MyComponent", function() {
+    o("returns a div", function() {
+        var vnode = MyComponent.view()
+
+        o(vnode.tag).equals("div")
+        o(vnode.children.length).equals(1)
+        o(vnode.children[0].tag).equals("#")
+        o(vnode.children[0].children).equals("Hello world")
+    })
+})
+
+

Typically, you wouldn't test the structure of the vnode tree so granularly, and you would instead only test non-trivial, dynamic aspects of the view. A tool that can help making testing easier with deep vnode trees is Mithril Query.

+

Sometimes, you need to mock the dependencies of a module in order to test the module in isolation. Mockery is one tool that allows you to do that.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

trust(html)

+ +
+

Description

+

Turns an HTML string into unescaped HTML. Do not use m.trust on unsanitized user input.

+

Always try to use an alternative method first, before considering using m.trust.

+
+

Signature

+

vnode = m.trust(html)

+ + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
htmlStringYesA string containing HTML text
returnsVnodeA trusted HTML vnode that represents the input string
+

How to read signatures

+
+

How it works

+

By default, Mithril escapes all values in order to prevent a class of security problems called XSS injections.

+
var userContent = "<script>alert('evil')</script>"
+var view = m("div", userContent)
+
+m.render(document.body, view)
+
+// equivalent HTML
+// <div>&lt;script&gt;alert('evil')&lt;/script&gt;</div>
+
+

However, sometimes it is desirable to render rich text and formatting markup. To fill that need, m.trust creates trusted HTML vnodes which are rendered as HTML.

+
var view = m("div", [
+    m.trust("<h1>Here's some <em>HTML</em></h1>")
+])
+
+m.render(document.body, view)
+
+// equivalent HTML
+// <div><h1>Here's some <em>HTML</em></h1></div>
+
+

Trusted HTML vnodes are objects, not strings; therefore they cannot be concatenated with regular strings.

+
+

Security considerations

+

You must sanitize the input of m.trust to ensure there's no user-generated malicious code in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous javascript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.

+

There are many ways in which an HTML string may contain executable code. The most common ways to inject security attacks are to add an onload or onerror attributes in <img> or <iframe> tags, and to use unbalanced quotes such as " onerror="alert(1) to inject executable contexts in unsanitized string interpolations.

+
var data = {}
+
+// Sample vulnerable HTML string
+var description = "<img alt='" + data.title + "'> <span>" + data.description + "</span>"
+
+// An attack using javascript-related attributes
+data.description = "<img onload='alert(1)'>"
+
+// An attack using unbalanced tags
+data.description = "</span><img onload='alert(1)'><span"
+
+// An attack using unbalanced quotes
+data.title = "' onerror='alert(1)"
+
+// An attack using a different attribute
+data.title = "' onmouseover='alert(1)"
+
+// An attack that does not use javascript
+data.description = "<a href='http://evil.com/login-page-that-steals-passwords.html'>Click here to read more</a>"
+
+

There are countless non-obvious ways of creating malicious code, so it is highly recommended that you use a whitelist of permitted HTML tags, attributes and attribute values, as opposed to a blacklist to sanitize the user input. It's also highly recommended that you use a proper HTML parser, instead of regular expressions for sanitization, because regular expressions are extremely difficult to test for all edge cases.

+
+

Scripts that do not run

+

Even though there are many obscure ways to make an HTML string run Javascript, <script> tags are one thing that does not run when it appears in an HTML string.

+

For historical reasons, browsers ignore <script> tags that are inserted into the DOM via innerHTML. They do this because once the element is ready (and thus, has an accessible innerHTML property), the rendering engines cannot backtrack to the parsing-stage if the script calls something like document.write("").

+

This browser behavior may seem surprising to a developer coming from jQuery, because jQuery implements code specifically to find script tags and run them in this scenario. Mithril follows the browser behavior. If jQuery behavior is desired, you should consider either moving the code out of the HTML string and into an oncreate lifecycle method, or use jQuery (or re-implement its script parsing code).

+
+

Avoid trusting HTML

+

As a general rule of thumb, you should avoid using m.trust unless you are explicitly rendering rich text and there's no other way to get the results that you want.

+
// AVOID
+m("div", m.trust("hello world"))
+
+// PREFER
+m("div", "hello world")
+
+

Avoid blind copying and pasting

+

One common way to misuse m.trust is when working with third party services whose tutorials include HTML code to be copied and pasted. In most cases, HTML should be written using vnodes (typically via the m() utility)

+

Here's the example snippet for the Facebook Like button:

+
<!-- Load Facebook SDK for JavaScript -->
+<div id="fb-root"></div>
+<script>(function(d, s, id) {
+  var js, fjs = d.getElementsByTagName(s)[0];
+  if (d.getElementById(id)) return;
+  js = d.createElement(s); js.id = id;
+  js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1";
+  fjs.parentNode.insertBefore(js, fjs);
+}(document, 'script', 'facebook-jssdk'));</script>
+
+<!-- Your like button code -->
+<div class="fb-like"
+    data-href="http://www.your-domain.com/your-page.html"
+    data-layout="standard"
+    data-action="like"
+    data-show-faces="true">
+</div>
+
+

And here's how to refactor into a Mithril component in a way that avoids m.trust:

+
var FacebookLikeButton = {
+    oncreate: function() {
+        (function(d, s, id) {
+          var js, fjs = d.getElementsByTagName(s)[0];
+          if (d.getElementById(id)) return;
+          js = d.createElement(s); js.id = id;
+          js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1";
+          fjs.parentNode.insertBefore(js, fjs);
+        }(document, 'script', 'facebook-jssdk'));
+    },
+    view: function() {
+        return [
+            m("#fb-root"),
+            m("#fb-like[data-href=http://www.your-domain.com/your-page.html][data-layout=standard][data-action=like][data-show-faces=true]")
+        ]
+    }
+}
+
+

The Mithril component above simply copies the script tag's code into the oncreate hook and declares the remaining HTML tags using Mithril's m() syntax.

+

Avoid HTML entities

+

A common way to misuse m.trust is to use it for HTML entities. A better approach is to use the corresponding unicode characters:

+
// AVOID
+m("h1", "Coca-Cola", m.trust("&trade;"))
+
+// PREFER
+m("h1", "Coca-Cola™")
+
+

Unicode characters for accented characters can be typed using a keyboard layout for an applicable language, and one may also choose to memorize keyboard shortcuts to produce commonly used symbols (e.g. Alt+0153 in Windows, or Option+2 on Mac for the ™ symbol). Another simple method to produce them is to simply copy and paste the desired character from a unicode character table. Yet another related method is to type an escaped unicode codepoint (e.g. "\u2122" for the ™ symbol).

+

All characters that are representable as HTML entities have unicode counterparts, including non-visible characters such as &nbsp; and &shy;.

+

To avoid encoding issues, you should set the file encoding to UTF-8 on the Javascript file, as well as add the <meta charset="utf-8"> meta tag in the host HTML file.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

version

+ +
+

Signature

+

m.version

+ + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
returnsStringReturns the version number
+
+

How it works

+

The m.version property is a string containing the semver value for the current release.

+

Semver (or Semantic Versioning) specifies that a version number must follow the syntax "0.0.0", where the first number is the MAJOR number, the second is the MINOR number and the third is the PATCH number.

+
    +
  • The MAJOR number changes when there are backwards-incompatible API changes,
  • +
  • The MINOR number changes when functionality is added in a backwards-compatible manner, and
  • +
  • The PATCH number changes when there are backwards-compatible bug fixes
  • +
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

Virtual DOM nodes

+ +
+

What is virtual DOM

+

A virtual DOM tree is a Javascript data structure that describes a DOM tree. It consists of nested virtual DOM nodes, also known vnodes.

+

The first time a virtual DOM tree is rendered, it is used as a blueprint to create a DOM tree that matches its structure.

+

Typically, Virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril diffs a vnode tree against its previous version and only modifies DOM elements in spots where there are changes.

+

It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.

+

For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril also generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.

+

The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide retained mode rendering, a style of rendering that makes it drastically easier to manage UI complexity.

+

To illustrate why retained mode is so important, consider the DOM API and HTML. The DOM API is an immediate mode) rendering system and requires writing out exact instructions to assemble a DOM tree procedurally. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances for introducing bugs and more chances to make code harder to understand.

+

In contrast, HTML is a retained mode rendering system. With HTML, you can write a DOM tree in a far more natural and readable way, without worrying about forgetting to append a child to a parent, running into stack overflows when rendering extremely deep trees, etc.

+

Virtual DOM goes one step further than HTML by allowing you to write dynamic DOM trees without having to manually write multiple sets of DOM API calls to efficiently synchronize the UI to arbitrary data changes.

+
+

Basics

+

Virtual DOM nodes, or vnodes, are javascript objects that represent DOM elements (or parts of the DOM). Mithril's virtual DOM engine consumes a tree of vnodes to produce a DOM tree.

+

Vnodes can be created via the m() hyperscript utility:

+
m("div", {id: "test"}, "hello")
+
+

Vnodes can also consume components:

+
// define a component
+var ExampleComponent = {
+    view: function(vnode) {
+        return m("div", vnode.attrs, ["Hello ", vnode.children])
+    }
+}
+
+// consume it
+m(ExampleComponent, {style: "color:red;"}, "world")
+
+// equivalent HTML:
+// <div style="color:red;">Hello world</div>
+
+
+

Structure

+

Virtual DOM nodes, or vnodes, are Javascript objects that represent an element (or parts of the DOM) and have the following properties:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
tagString|ObjectThe nodeName of a DOM element. It may also be the string [ if a vnode is a fragment, # if it's a text vnode, or < if it's a trusted HTML vnode. Additionally, it may be a component.
keyString?The value used to map a DOM element to its respective item in a array of data.
attrsObject?A hashmap of DOM attributes, events, properties and lifecycle methods.
children(Array|String|Number|Boolean)?In most vnode types, the children property is an array of vnodes. For text and trusted HTML vnodes, The children property is either a string, a number or a boolean.
text(String|Number|Boolean)?This is used instead of children if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the text property even if they have a text node as its only child.
domElement?Points to the element that corresponds to the vnode. This property is undefined in the oninit lifecycle method. In fragment and trusted HTML vnodes, dom points to the first element in the range.
domSizeNumber?This is only set in fragment and trusted HTML vnodes, and it's undefined in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the dom property).
stateObjectAn object that is persisted between redraws. In component vnodes, state is a shallow clone of the component object.
eventsObject?An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The events property is undefined if there are no event handlers defined. This property is only used internally by Mithril, do not use it.
+
+

Vnode types

+

The tag property of a vnode determines its type. There are five vnode types:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Vnode typeExampleDescription
Element{tag: "div"}Represents a DOM element.
Fragment{tag: "[", children: []}Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the m() helper function, fragment vnodes can only be created by nesting arrays into the children parameter of m(). m("[") does not create a valid vnode.
Text{tag: "#", children: ""}Represents a DOM text node.
Trusted HTML{tag: "<", children: "<br>"}Represents a list of DOM elements from an HTML string.
Component{tag: ExampleComponent}If tag is a Javascript object with a view method, the vnode represents the DOM generated by rendering the component.
+

Everything in a virtual DOM tree is a vnode, including text. The m() utility automatically normalizes its children argument and turns strings into text vnodes and nested arrays into fragment vnodes.

+

Only element tag names and components can be the first argument of the m() function. In other words, [, # and < are not valid selector arguments for m(). Trusted HTML vnodes can be created via m.trust()

+
+

Monomorphic class

+

The mithril/render/vnode module is used by Mithril to generate all vnodes. This ensures modern Javascript engines can optimize virtual dom diffing by always compiling vnodes to the same hidden class.

+

When creating libraries that emit vnodes, you should use this module instead of writing naked Javascript objects in order to ensure a high level of rendering performance.

+
+

Avoid anti-patterns

+

Avoid memoizing mutable vnodes

+

Vnodes are supposed to represent the state of the DOM at a certain point in time. Mithril's rendering engine assumes a reused vnode is unchanged, so modifying a vnode that was used in a previous render will result in undefined behavior.

+

It is possible to reuse vnodes to prevent a diff, but it's preferable to use the onbeforeupdate hook to make your intent clear to other developers (or your future self).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + Mithril.js + + + + + +
+
+

Mithril 1.0.0-rc.8

+ +
+
+
+
+

withAttr(attrName, callback)

+ +
+

Description

+

Returns an event handler that runs callback with the value of the specified DOM attribute

+
var state = {
+    value: "",
+    setValue: function(v) {state.value = v}
+}
+
+var Component = {
+    view: function() {
+        return m("input", {
+            oninput: m.withAttr("value", state.setValue),
+            value: state.value,
+        })
+    }
+}
+
+m.mount(document.body, Component)
+
+
+

Signature

+

m.withAttr(attrName, callback, thisArg?)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
attrNameStringYesThe name of the attribute or property whose value will be used
callbackany -> undefinedYesThe callback
thisArganyNoAn object to bind to the this keyword in the callback function
returnsEvent -> undefinedAn event handler function
+

How to read signatures

+
+

How it works

+

The m.withAttr method creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument.

+

This helper function is provided to help decouple the browser's event model from application code.

+
// standalone usage
+document.body.onclick = m.withAttr("title", function(value) {
+    console.log(value) // logs the title of the <body> element when clicked
+})
+
+

Typically, m.withAttr() can be used in Mithril component views to avoid polluting the data layer with DOM event model concerns:

+
var state = {
+    email: "",
+    setEmail: function(email) {
+        state.email = email.toLowerCase()
+    }
+}
+
+var MyComponent = {
+    view: function() {
+        return m("input", {
+            oninput: m.withAttr("value", state.setEmail),
+            value: state.email
+        })
+    }
+}
+
+m.mount(document.body, MyComponent)
+
+
+

Predictable event target

+

The m.withAttr() helper reads the value of the element to which the event handler is bound, which is not necessarily the same as the element where the event originated.

+
var state = {
+    url: "",
+    setURL: function(url) {state.url = url}
+}
+
+var MyComponent = {
+    view: function() {
+        return m("a[href='/foo']", {onclick: m.withAttr("href", state.setURL)}, [
+            m("span", state.url)
+        ])
+    }
+}
+
+m.mount(document.body, MyComponent)
+
+

In the example above, if the user clicks on the text within the link, e.target will point to the <span>, not the <a>.

+

While this behavior works as per its specs, it's not very intuitive or useful most of the time. Therefore, m.withAttr uses the value of e.currentTarget which does point to the <a>, as one would normally expect.

+
+

Attributes and properties

+

The first argument of m.withAttr() can be either an attribute or a property.

+
// reads from `select.selectedIndex` property
+var state = {
+    index: 0,
+    setIndex: function(index) {state.index = index}
+}
+m("select", {onclick: m.withAttr("selectedIndex", state.setIndex)})
+
+

If a value can be both an attribute and a property, the property value is used.

+
// value is a boolean, because the `input.checked` property is boolean
+var state = {
+    selected: false,
+    setSelected: function(selected) {state.selected = selected}
+}
+m("input[type=checkbox]", {onclick: m.withAttr("checked", state.setSelected)})
+
+ +
+ License: MIT. © Leo Horie. +
+
+ + +([^<]+?)<\/h5>/gim, function(match, id, text) { // fix anchors return "
" + text + "
" }) - fs.writeFileSync("docs/archive/" + version + "/" + outputFilename.replace(/^docs\//, ""), html, "utf-8") + fs.writeFileSync("archive/v" + version + "/" + outputFilename.replace(/^docs\//, ""), html, "utf-8") } - else { - fs.writeFileSync("docs/archive/" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") + else if (!pathname.match(/lint|generate/)) { + fs.writeFileSync("archive/v" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") } } } - diff --git a/docs/guides.md b/docs/guides.md index 5674f41b..3a8c8313 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -1,6 +1,6 @@ - Tutorials - [Installation](installation.md) - - [Introduction](introduction.md) + - [Introduction](index.md) - [Tutorial](simple-application.md) - Resources - [JSX](jsx.md) diff --git a/docs/introduction.md b/docs/index.md similarity index 80% rename from docs/introduction.md rename to docs/index.md index a8e78a18..d6d41ddc 100644 --- a/docs/introduction.md +++ b/docs/index.md @@ -12,15 +12,46 @@ ### What is Mithril? -Mithril is a client-side Javascript framework for building Single Page Applications. +Mithril is a modern client-side Javascript framework for building Single Page Applications. It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box. + +
+
+
Download size
+ Mithril (8kb) +
+ Vue + Vue-Router + Vuex + fetch (40kb) +
+ React + React-Router + Redux + fetch (64kb) +
+ Angular (135kb) +
+
+
+
Performance
+ Mithril (6.4ms) +
+ Vue (9.8ms) +
+ React (12.1ms) +
+ Angular (11.5ms) +
+
+
+ +Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess. + If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. --- -Note: This introduction assumes you have basic level of Javacript knowledge. If you don't, there are many great resources to learn. [Speaking Javascript](http://speakingjs.com/es5/index.html) is a good e-book for absolute beginners. If you're already familiar with other programming languages, the [Eloquent Javascript](http://eloquentjavascript.net/) e-book might be more suitable for you. [Codecademy](https://www.codecademy.com/learn/javascript) is another good resource that emphasizes learning via interactivity. - ### Getting started The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes. @@ -142,7 +173,8 @@ var Hello = { view: function() { return m("main", [ m("h1", {class: "title"}, "My first app"), - m("button", {onclick: function() {count++}}, count + " clicks"), // changed this line + // changed the next line + m("button", {onclick: function() {count++}}, count + " clicks"), ]) } } diff --git a/docs/layout.html b/docs/layout.html index b6e1c504..86cc52ac 100644 --- a/docs/layout.html +++ b/docs/layout.html @@ -11,7 +11,7 @@

Mithril [version]