diff --git a/dist/CNAME b/dist/CNAME new file mode 100644 index 00000000..5c153480 --- /dev/null +++ b/dist/CNAME @@ -0,0 +1 @@ +mithril.js.org diff --git a/dist/animation.html b/dist/animation.html new file mode 100644 index 00000000..b882f536 --- /dev/null +++ b/dist/animation.html @@ -0,0 +1,151 @@ + + + + Animations - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 is 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 the 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) {
+            vnode.dom.addEventListener("animationend", resolve)
+        })
+    },
+    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 when the animationend event fires. 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 for the exit animation to finish.

+

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. If you want to animate a box-shadow, consider putting the box-shadow rule on a pseudo element, and animate that element's opacity instead. Other things that can be expensive include large or dynamically scaled images and overlapping elements with different position values (e.g. an absolute positioned element over a fixed element).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/api.html b/dist/api.html new file mode 100644 index 00000000..9635792a --- /dev/null +++ b/dist/api.html @@ -0,0 +1,156 @@ + + + + API - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/CNAME b/dist/archive/v2.0.0-rc.5/CNAME new file mode 100644 index 00000000..5c153480 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/CNAME @@ -0,0 +1 @@ +mithril.js.org diff --git a/dist/archive/v2.0.0-rc.5/animation.html b/dist/archive/v2.0.0-rc.5/animation.html new file mode 100644 index 00000000..b882f536 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/animation.html @@ -0,0 +1,151 @@ + + + + Animations - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 is 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 the 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) {
+            vnode.dom.addEventListener("animationend", resolve)
+        })
+    },
+    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 when the animationend event fires. 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 for the exit animation to finish.

+

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. If you want to animate a box-shadow, consider putting the box-shadow rule on a pseudo element, and animate that element's opacity instead. Other things that can be expensive include large or dynamically scaled images and overlapping elements with different position values (e.g. an absolute positioned element over a fixed element).

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/api.html b/dist/archive/v2.0.0-rc.5/api.html new file mode 100644 index 00000000..9635792a --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/api.html @@ -0,0 +1,156 @@ + + + + API - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/autoredraw.html b/dist/archive/v2.0.0-rc.5/autoredraw.html new file mode 100644 index 00000000..ffa1b2a1 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/autoredraw.html @@ -0,0 +1,168 @@ + + + + The auto-redraw system - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, raw Promise resolutions 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.

+
function StableComponent() {
+    var height = 0
+
+    return {
+        oncreate: function(vnode) {
+            height = vnode.dom.offsetHeight
+            m.redraw()
+        },
+        view: function() {
+            return m("div", "This component is " + 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/buildPathname.html b/dist/archive/v2.0.0-rc.5/buildPathname.html new file mode 100644 index 00000000..12497336 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/buildPathname.html @@ -0,0 +1,106 @@ + + + + buildPathname(object) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

buildPathname(object)

+ +
+

Description

+

Turns a path template and a parameters object into a string of form /path/user?a=1&b=2

+
var querystring = m.buildPathname("/path/:id", {id: "user", a: "1", b: "2"})
+// "/path/user?a=1&b=2"
+
+

Signature

+

querystring = m.buildPathname(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.buildPathname creates a path name from a path template and a parameters object. It's useful for building URLs, and it's what m.route, m.request, and m.jsonp all use internally to interpolate paths. It uses m.buildQueryString to generate the query parameters to append to the path name.

+
var querystring = m.buildPathname("/path/:id", {id: "user", a: 1, b: 2})
+
+// querystring is "/path/user?a=1&b=2"
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/buildQueryString.html b/dist/archive/v2.0.0-rc.5/buildQueryString.html new file mode 100644 index 00000000..bf5c5932 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/buildQueryString.html @@ -0,0 +1,116 @@ + + + + buildQueryString(object) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/change-log.html b/dist/archive/v2.0.0-rc.5/change-log.html new file mode 100644 index 00000000..4f6f5124 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/change-log.html @@ -0,0 +1,739 @@ + + + + Change log - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Change log

+ +
+

Upcoming...

+

v2.0.0-rc

+

Breaking changes

+ +

News

+ +

Bug fixes

+ +
+

v1.2.0

+

News

+ +

Bug fixes

+ +

Note

+ +
+

v1.1.6

+

Bug fixes

+ +

Ospec improvements

+ +
+

v1.1.5

+

Bug fixes

+ +
+

v1.1.4

+

Bug fixes

+ +

Ospec improvements:

+ +
+

v1.1.3

+

Bug fixes

+ +
+

v1.1.2

+

Bug fixes

+ +
+

Docs / Repo maintenance

+

Our thanks to @0joshuaolson1, @ACXgit, @cavemansspa, @CreaturesInUnitards, @dlepaux, @isaaclyman, @kevinkace, @micellius, @spacejack and @yurivish

+

Other

+ +
+

v1.1.1

+

Bug fixes

+ +
+

v1.1.0

+

News

+ +

Bug fixes

+ +
+

v1.0.1

+

News

+ +

Bug fixes

+ +
+

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"
+        }
+    }
+})
+
+

Building/Parsing query strings

+

v0.2.x used methods hanging off of m.route, m.route.buildQueryString() and m.route.parseQueryString(). In v1.x these have been broken out and attached to the root m.

+

v0.2.x

+
var qs = m.route.buildQueryString({ a : 1 });
+
+var obj = m.route.parseQueryString("a=1");
+

v1.x

+
var qs = m.buildQueryString({ a : 1 });
+
+var obj = m.parseQueryString("a=1");
+
+

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("/")}})
+    }
+}
+
+

Run code on component removal

+

Components no longer call this.onunload when they are being removed. They now use the standardized lifecycle hook onremove.

+

v0.2.x

+
var Component = {
+    controller: function() {
+        this.onunload = function(e) {
+            // ...
+        }
+    },
+    view: function() {
+        // ...
+    }
+}
+

v1.x

+
var Component = {
+    onremove : function() {
+        // ...
+    }
+    view: function() {
+        // ...
+    }
+}
+
+

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 = function() {
+    return 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/code-of-conduct.html b/dist/archive/v2.0.0-rc.5/code-of-conduct.html new file mode 100644 index 00000000..419b659a --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/code-of-conduct.html @@ -0,0 +1,142 @@ + + + + Contributor Covenant Code of Conduct - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Contributor Covenant Code of Conduct

+ +

Our Pledge

+

In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation.

+

Our Standards

+

Examples of behavior that contributes to creating a positive environment +include:

+ +

Examples of unacceptable behavior by participants include:

+ +

Our Responsibilities

+

Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior.

+

Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful.

+

Scope

+

This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers.

+

Enforcement

+

Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at github@patcavit.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately.

+

Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership.

+

Attribution

+

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at http://contributor-covenant.org/version/1/4

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/components.html b/dist/archive/v2.0.0-rc.5/components.html new file mode 100644 index 00000000..3c0a5a5f --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/components.html @@ -0,0 +1,597 @@ + + + + Components - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

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

Lifecycle methods

+

Components can have the same lifecycle methods as virtual DOM nodes. Note that vnode is passed as an argument to each lifecycle method, as well as to view (with the previous vnode passed additionally to onbeforeupdate):

+
var ComponentWithHooks = {
+    oninit: function(vnode) {
+        console.log("initialized")
+    },
+    oncreate: function(vnode) {
+        console.log("DOM created")
+    },
+    onbeforeupdate: function(newVnode, oldVnode) {
+        return true
+    },
+    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")
+    },
+    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(vnode) {
+    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.

+
+

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 defined in the attrs object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril itself. Use them in attrs only when you specifically wish to use them as lifecycle methods.

+
+

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.

+

Note that unlike many other frameworks, mutating component state does not trigger redraws or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by m.request complete or when the browser navigates to different routes. Mithril's component state mechanisms simply exist as a convenience for applications.

+

If a state change occurs that is not as a result of any of the above conditions (e.g. after a setTimeout), then you can use m.redraw() to trigger a redraw manually.

+

Closure Component State

+

In the above examples, each component is defined as a POJO (Plain Old JavaScript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a closure component, which is simply a wrapper function which returns a POJO component instance, which in turn carries its own, closed-over scope.

+

With a closure component, state can simply be maintained by variables that are declared within the outer function:

+
function ComponentWithState(initialVnode) {
+    // Component state variable, unique to each instance
+    var count = 0
+
+    // POJO component instance: any object with a
+    // view function which returns a vnode
+    return {
+        oninit: function(vnode){
+            console.log("init a closure component")
+        },
+        view: function(vnode) {
+            return m("div",
+                m("p", "Count: " + count),
+                m("button", {
+                    onclick: function() {
+                        count += 1
+                    }
+                }, "Increment count")
+            )
+        }
+    }
+}
+

Any functions declared within the closure also have access to its state variables.

+
function ComponentWithState(initialVnode) {
+    var count = 0
+
+    function increment() {
+        count += 1
+    }
+
+    function decrement() {
+        count -= 1
+    }
+
+    return {
+        view: function(vnode) {
+            return m("div",
+                m("p", "Count: " + count),
+                m("button", {
+                    onclick: increment
+                }, "Increment"),
+                m("button", {
+                    onclick: decrement
+                }, "Decrement")
+            )
+        }
+    }
+}
+

Closure components are consumed in the same way as POJOs, e.g. m(ComponentWithState, { passedData: ... }).

+

A big advantage of closure components is that we don't need to worry about binding this when attaching event handler callbacks. In fact this is never used at all and we never have to think about this context ambiguities.

+
+

POJO Component State

+

It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via vnode.state and via the this keyword in component methods.

+

At initialization

+

For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of vnode.state. This allows simple "blueprint" state initialization.

+

In the example below, data becomes a property of the ComponentWithInitialState component's vnode.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

+

As you can see, 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.

+
+

ES6 classes

+

If it suits your needs (like in object-oriented projects), components can also be written using ES6 class syntax:

+
class ES6ClassComponent {
+    constructor(vnode) {
+        this.kind = "ES6 class"
+    }
+    view() {
+        return m("div", `Hello from an ${this.kind}`)
+    }
+    oncreate() {
+        console.log(`A ${this.kind} component was created`)
+    }
+}
+

Component classes must define a view() method, detected via .prototype.view, to get the tree to render.

+

They can be consumed in the same way regular components can.

+
// EXAMPLE: via m.render
+m.render(document.body, m(ES6ClassComponent))
+
+// EXAMPLE: via m.mount
+m.mount(document.body, ES6ClassComponent)
+
+// EXAMPLE: via m.route
+m.route(document.body, "/", {
+    "/": ES6ClassComponent
+})
+
+// EXAMPLE: component composition
+class AnotherES6ClassComponent {
+    view() {
+        return m("main", [
+            m(ES6ClassComponent)
+        ])
+    }
+}
+

Class Component State

+

With classes, state can be managed by class instance properties and methods, and accessed via this:

+
class ComponentWithState {
+    constructor(vnode) {
+        this.count = 0
+    }
+    increment() {
+        this.count += 1
+    }
+    decrement() {
+        this.count -= 1
+    }
+    view() {
+        return m("div",
+            m("p", "Count: " + count),
+            m("button", {
+                onclick: () => {this.increment()}
+            }, "Increment"),
+            m("button", {
+                onclick: () => {this.decrement()}
+            }, "Decrement")
+        )
+    }
+}
+

Note that we must wrap the event callbacks in arrow functions so that the this context is preserved correctly.

+
+

Mixing component kinds

+

Components can be freely mixed. A class component can have closure or POJO components as children, etc...

+
+

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: function (e) { this.setUsername(e.target.value) },
+                value: this.username,
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { this.setPassword(e.target.value) },
+                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 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: function (e) { Auth.setUsername(e.target.value) },
+                value: Auth.username
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { Auth.setPassword(e.target.value) },
+                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.

+

Don't forward vnode.attrs itself to other vnodes

+

Sometimes, you might want to keep an interface flexible and your implementation simpler by forwarding attributes to a particular child component or element, in this case Bootstrap's modal. It might be tempting to forward a vnode's attributes like this:

+
// AVOID
+var Modal = {
+    // ...
+    view: function(vnode) {
+        return m(".modal[tabindex=-1][role=dialog]", vnode.attrs, [
+            //         forwarding `vnode.attrs` here ^
+            // ...
+        ])
+    }
+}
+

If you do it like above, you could run into issues when using it:

+
var MyModal = {
+    view: function() {
+        return m(Modal, {
+            // This toggles it twice, so it doesn't show
+            onupdate: function(vnode) {
+                if (toggle) $(vnode.dom).modal("toggle")
+            }
+        }, [
+            // ...
+        ])
+    }
+}
+

Instead, you should forward single attributes into vnodes:

+
// PREFER
+var Modal = {
+    // ...
+    view: function(vnode) {
+        return m(".modal[tabindex=-1][role=dialog]", vnode.attrs.attrs, [
+            //              forwarding `attrs:` here ^
+            // ...
+        ])
+    }
+}
+
+// Example
+var MyModal = {
+    view: function() {
+        return m(Modal, {
+            attrs: {
+                // This toggles it once
+                onupdate: function(vnode) {
+                    if (toggle) $(vnode.dom).modal("toggle")
+                }
+            },
+            // ...
+        })
+    }
+}
+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/contributing.html b/dist/archive/v2.0.0-rc.5/contributing.html new file mode 100644 index 00000000..f49a478d --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/contributing.html @@ -0,0 +1,148 @@ + + + + Contributing FAQs - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Contributing FAQs

+ +

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:

+ +

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 or Bublé? Would a PR to upgrade be welcome?

+

Being able to run Mithril's raw source code in all supported browsers is a requirement for all browser-related modules in this repo. In addition, transpiled code is generally much 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. Besides, we aren't the only one who've decided to drop the semicolon. (We don't use Standard, though.)

+

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.

+

Do you all accept donations?

+

Yes, we do, over at our OpenCollective page. We don't actively seek donations, but they are very much appreciated and are used to support development and related expenses. Both one-time and recurring donations are accepted.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/credits.html b/dist/archive/v2.0.0-rc.5/credits.html new file mode 100644 index 00000000..83530afe --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/credits.html @@ -0,0 +1,109 @@ + + + + Credits - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

+ +

Other people who also deserve recognition:

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/css.html b/dist/archive/v2.0.0-rc.5/css.html new file mode 100644 index 00000000..9b453555 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/css.html @@ -0,0 +1,151 @@ + + + + CSS - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

+ +
+

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 are 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 cryptic class names).

+

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

+

If you are adamant about using a CSS-in-JS library, consider using J2C, which works without configuration and implements @keyframes and @font-face.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/es6.html b/dist/archive/v2.0.0-rc.5/es6.html new file mode 100644 index 00000000..23febcb1 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/es6.html @@ -0,0 +1,167 @@ + + + + ES6 - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 an 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

+
const path = require('path')
+
+module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: path.resolve(__dirname, './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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/examples.html b/dist/archive/v2.0.0-rc.5/examples.html new file mode 100644 index 00000000..bf48ab26 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/examples.html @@ -0,0 +1,95 @@ + + + + Examples - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Examples

+ +

Here are some examples of Mithril in action

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/favicon.ico b/dist/archive/v2.0.0-rc.5/favicon.ico new file mode 100644 index 00000000..61807fae Binary files /dev/null and b/dist/archive/v2.0.0-rc.5/favicon.ico differ diff --git a/dist/archive/v2.0.0-rc.5/favicon.png b/dist/archive/v2.0.0-rc.5/favicon.png new file mode 100644 index 00000000..50712630 Binary files /dev/null and b/dist/archive/v2.0.0-rc.5/favicon.png differ diff --git a/dist/archive/v2.0.0-rc.5/fragment.html b/dist/archive/v2.0.0-rc.5/fragment.html new file mode 100644 index 00000000..c5ef9e15 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/fragment.html @@ -0,0 +1,142 @@ + + + + fragment(attrs, children) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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
attrsObjectNoHTML attributes or element properties
childrenArray<Vnode>|String|Number|BooleanNoChild vnodes. Can be written as splat arguments
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 and don't require keyed logic, you should use m() instead.

+

Normally you can use simple arrays or splats 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/framework-comparison.html b/dist/archive/v2.0.0-rc.5/framework-comparison.html new file mode 100644 index 00000000..174dcd31 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/framework-comparison.html @@ -0,0 +1,264 @@ + + + + Framework comparison - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. This 10 minute 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.

+ +

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 encouraged 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:

+ +

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:

+ +

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 a Vue 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. All component creation styles in Mithril output the same vnode structure using native JavaScript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup.

+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/hyperscript.html b/dist/archive/v2.0.0-rc.5/hyperscript.html new file mode 100644 index 00000000..e4b2fc6c --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/hyperscript.html @@ -0,0 +1,487 @@ + + + + m(selector, attributes, children) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, attrs, children)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
selectorString|ObjectYesA CSS selector or a component
attrsObjectNoHTML 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 attrs 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>
+

Attributes passed as the second argument

+

You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details).

+
m("button", {
+    class: "my-button",
+    onclick: function() {/* ... */},
+    oncreate: function() {/* ... */}
+})
+

If the value of such an attribute is null or undefined, it is treated as if the attribute was absent.

+

If there are class names in both first and second arguments of m(), they are merged together as you would expect. If the value of the class in the second argument is nullor undefined, it is ignored.

+

If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is null or undefined.

+
+

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]")
+

This even includes custom elements. For example, you can use A-Frame within Mithril, no problem!

+
m("a-scene", [
+    m("a-box", {
+        position: "-1 0.5 -3",
+        rotation: "0 45 0",
+        color: "#4CC3D9",
+    }),
+
+    m("a-sphere", {
+        position: "0 1.25 -5",
+        radius: "1.25",
+        color: "#EF2D5E",
+    }),
+
+    m("a-cylinder", {
+        position: "1 0.75 -3",
+        radius: "0.5",
+        height: "1.5",
+        color: "#FFC65D",
+    }),
+
+    m("a-plane", {
+        position: "0 0 -4",
+        rotation: "-90 0 0",
+        width: "4",
+        height: "4",
+        color: "#7BC8A4",
+    }),
+
+    m("a-sky", {
+        color: "#ECECEC",
+    }),
+])
+

And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using Brick's brick-deck as an example, they have a selected-index attribute with a corresponding selectedIndex getter/setter property.

+
m("brick-deck[selected-index=0]", [/* ... */]) // lowercase
+m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase
+// I know these look odd, but `brick-deck`'s `selectedIndex` property is a
+// string, not a number.
+m("brick-deck", {"selected-index": "0"}, [/* ... */])
+m("brick-deck", {"selectedIndex": "0"}, [/* ... */])
+

For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element my-special-element that has an elem.whitelist array getter/setter property, you could do this, and it'd work as you'd expect:

+
m("my-special-element", {
+    whitelist: [
+        "https://example.com",
+        "http://neverssl.com",
+        "https://google.com",
+    ],
+})
+

If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example:

+
// These two are equivalent
+m("a-entity#player")
+m("a-entity", {id: "player"})
+

Do note that all the properties with magic semantics, like lifecycle attributes, onevent handlers, keys, class, and style, those are still treated the same way they are for normal HTML elements.

+
+

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.

+

You can use both hyphenated CSS property names (like background-color) and camel cased DOM style property names (like backgroundColor). You can also define CSS custom properties, if your browser supports them.

+

Mithril does not attempt to add units to number values. It simply stringifies them.

+
+

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

Mithril accepts functions and EventListener objects. So this will also work:

+
var clickListener = {
+    handleEvent: function(e) {
+        console.log(e)
+    }
+}
+
+m("div", {onclick: clickListener})
+

By default, when an event attached with hyperscript fires, this will trigger Mithril's auto-redraw after your event callback returns (assuming you are using m.mount or m.route instead of m.render directly). You can disable auto-redraw specifically for a single event by setting e.redraw = false on it:

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

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(vnode) {
+        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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/index.html b/dist/archive/v2.0.0-rc.5/index.html new file mode 100644 index 00000000..31d9a943 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/index.html @@ -0,0 +1,279 @@ + + + + Introduction - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+

Mithril supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required.

+

Looking for the v1 docs? Click here.

+
+

Getting started

+

An easy 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>
+    <script src="https://unpkg.com/mithril@next/mithril.js"></script>
+    <script>
+    var root = document.body
+
+    // your code goes here!
+    </script>
+</body>
+

To make things simpler you can fork this pen which already has the latest version of mithril loaded.

+

See the Pen Mithril Scaffold by Pat Cavit (@tivac) on CodePen.

+ + +

Mithril is also loaded onto this page already, so you can start poking at the m object in the developer console right away if you'd like!

+
+

Hello world

+

Let's start as small as we 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.

+

Live Example

+

See the Pen Mithril Hello World by Pat Cavit (@tivac) on CodePen.

+ + +
+

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 need 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"),
+])
+

Live Example

+

See the Pen Simple Mithril Example by Pat Cavit (@tivac) on CodePen.

+ + +

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.

+

Live Example

+

See the Pen Mithril Component Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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're 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.

+

Live Example

+

See the Pen Mithril Routing Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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 withCredentials means to enable cookies (a requirement for the REM API to work)

+
var count = 0
+var increment = function() {
+    m.request({
+        method: "PUT",
+        url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
+        data: {count: count + 1},
+        withCredentials: 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.

+

Live Example

+

See the Pen Mithril XHR Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/installation.html b/dist/archive/v2.0.0-rc.5/installation.html new file mode 100644 index 00000000..8c4ae48a --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/installation.html @@ -0,0 +1,245 @@ + + + + Installation - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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="https://unpkg.com/mithril@next/mithril.js"></script>
+
+

NPM

+
$ npm install mithril@next --save
+
+

Quick start with Webpack

+
    +
  1. Initialize the directory as an npm package

    +
    $ npm init --yes
    +
  2. +
  3. install required tools

    +
    $ npm install mithril@next --save
    +$ npm install webpack webpack-cli --save-dev
    +
  4. +
  5. Add a "start" entry to the scripts section in package.json.

    +
    {
    + // ...
    + "scripts": {
    +     "start": "webpack src/index.js --output bin/app.js -d --watch"
    + }
    +}
    +
  6. +
  7. Create src/index.js file.

    +
    import m from "mithril";
    +m.render(document.body, "hello world");
    +
  8. +
  9. create index.html

    +
    <!DOCTYPE html>
    +<body>
    + <script src="bin/app.js"></script>
    +</body>
    +
  10. +
  11. run bundler

    +
    $ npm start
    +
  12. +
  13. open index.html in a browser

    +
  14. +
+

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@next --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 webpack-cli --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 --output 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 --output bin/app.js -d --watch",
+        "build": "webpack src/index.js --output 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@next --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 src/index.js --output bin/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="https://unpkg.com/mithril@next/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")
+
+

TypeScript

+

TypeScript type definitions are available from DefinitelyTyped. They can be installed with:

+
$ npm install @types/mithril --save-dev
+

For example usage, to file issues or to discuss TypeScript related topics visit: https://github.com/MithrilJS/mithril.d.ts

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/integrating-libs.html b/dist/archive/v2.0.0-rc.5/integrating-libs.html new file mode 100644 index 00000000..6ee1e2f1 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/integrating-libs.html @@ -0,0 +1,197 @@ + + + + 3rd Party Integration - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

3rd Party Integration

+ +

Integration with third party libraries or vanilla JavaScript code can be achieved via lifecycle methods.

+

noUiSlider Example

+
/** NoUiSlider wrapper component */
+function Slider() {
+    var slider
+
+    return {
+        oncreate: function(vnode) {
+            // Initialize 3rd party lib here
+            slider = noUiSlider.create(vnode.dom, {
+                start: 0,
+                range: {min: 0, max: 100}
+            })
+            slider.on('update', function(values) {
+                vnode.attrs.onChange(values[0])
+                m.redraw()
+            })
+        },
+        onremove: function() {
+            // Cleanup 3rd party lib on removal
+            slider.destroy()
+        },
+        view: function() {
+            return m('div')
+        }
+    }
+}
+
+/** Demo app component */
+function Demo() {
+    var showSlider = false
+    var value = 0
+
+    return {
+        view: function() {
+            return m('.app',
+                m('p',
+                    m('button',
+                        {
+                            type: 'button',
+                            onclick: function() {
+                                showSlider = !showSlider
+                            }
+                        },
+                        showSlider ? "Destroy Slider" : "Create Slider"
+                    )
+                ),
+                showSlider && m(Slider, {
+                    onChange: function(v) {
+                        value = v
+                    }
+                }),
+                m('p', value)
+            )
+        }
+    }
+}
+
+m.mount(document.body, Demo)
+

Live Demo

+

Bootstrap FullCalendar Example

+
/** FullCalendar wrapper component */
+var FullCalendar = {
+    oncreate: function (vnode) {
+        console.log('FullCalendar::oncreate')
+        $(vnode.dom).fullCalendar({
+            // put your initial options and callbacks here
+        })
+    },
+    onremove: function (vnode) {
+        // Run any destroy / cleanup methods here.
+        $(vnode.dom).fullCalendar('destroy')
+    },
+    view: function (vnode) {
+        return m('div')
+    }
+}
+
+/** Demo app component */
+function Demo() {
+    var fullCalendarEl
+
+    function next() {
+        $(fullCalendarEl).fullCalendar('next')
+    }
+
+    function prev() {
+        $(fullCalendarEl).fullCalendar('prev')
+    }
+
+    return {
+        view: function (vnode) {
+            return [
+                m('h1', 'Calendar'),
+                m(FullCalendar, {
+                    oncreate: function(vnode) {
+                        fullCalendarEl = vnode.dom
+                    }
+                }),
+                m('button', {
+                    onclick: prev
+                }, 'Mithril Button -'),
+                m('button', {
+                    onclick: next
+                }, 'Mithril Button +')
+            ]
+        }
+    }
+}
+
+m.mount(document.body, Demo)
+

Live Demo

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/jsonp.html b/dist/archive/v2.0.0-rc.5/jsonp.html new file mode 100644 index 00000000..19bc44b9 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/jsonp.html @@ -0,0 +1,206 @@ + + + + jsonp(options) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
optionsObjectYesThe request options to pass.
options.urlStringYesThe path name to send the request to, optionally interpolated with values from options.data.
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)
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 type method
+

promise = m.jsonp(url, options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringYesThe path name to send the request to. options.url overrides this when present.
optionsObjectNoThe request options to pass.
returnsPromiseA promise that resolves to the response data, after it has been piped through the type method
+

This second form is mostly equivalent to m.jsonp(Object.assign({url: url}, options)), just it does not depend on the ES6 global Object.assign internally.

+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/jsx.html b/dist/archive/v2.0.0-rc.5/jsx.html new file mode 100644 index 00000000..0c354f2d --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/jsx.html @@ -0,0 +1,252 @@ + + + + JSX - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.render(document.body, <MyComponent />)
+// equivalent to m.render(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

+
const path = require('path')
+
+module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: path.resolve(__dirname, './bin'),
+        filename: 'app.js',
+    },
+    module: {
+        rules: [{
+            test: /\.js$/,
+            exclude: /node_modules/,
+            loader: 'babel-loader'
+        }]
+    }
+}
+

For those familiar with Webpack already, please note that adding the Babel options to the babel-loader section of your webpack.config.js will throw an error, so you need to include them in the separate .babelrc file.

+

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:

+ +

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/keys.html b/dist/archive/v2.0.0-rc.5/keys.html new file mode 100644 index 00000000..e6b3c140 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/keys.html @@ -0,0 +1,231 @@ + + + + Keys - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 make a user component, the key must be moved out of the component and put on the component itself, since it is now the immediate child of the array.

+
// AVOID
+var User = {
+    view: function(vnode) {
+        return m("div", { key: vnode.attrs.user.id }, [
+      m(Button, vnode.attrs.user.name)
+    ])
+    }
+}
+
+// PREFER
+users.map(function(u) {
+    return m(User, { key: u.id, user: u }) // 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"},
+]
+

Avoid mixing keyed and non-keyed vnodes in the same array

+

An array of vnodes must have only keyed vnodes or non-keyed vnodes, but not both. If you need to mix them, create a nested array.

+
// AVOID
+m("div", [
+    m("div", "a"),
+    m("div", {key: 1}, "b"),
+])
+
+// PREFER
+m("div", [
+    m("div", {key: 0}, "a"),
+    m("div", {key: 1}, "b"),
+])
+
+
+// PREFER
+m("div", [
+    m("div", "a"),
+    [
+        m("div", {key: 1}, "b"),
+    ]
+])
+

Avoid passing model data directly to components if the model uses key as a data property

+

The key property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose key property is expected to change over time. This can lead to components receiving the wrong data, re-initialize, or change positions unexpectedly. If your data model uses the key property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction:

+
// Data model
+var users = [
+    {id: 1, name: "John", key: 'a'},
+    {id: 2, name: "Mary", key: 'b'},
+]
+
+// Later on...
+users[0].key = 'c'
+
+// AVOID
+users.map(function(user){
+    // The component for John will be destroyed and recreated
+    return m(UserComponent, user)
+})
+
+// PREFER
+users.map(function(user){
+    // Key is specifically extracted: data model is given its own property
+    return m(UserComponent, {key: user.id, model: user})
+})
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/layout.html b/dist/archive/v2.0.0-rc.5/layout.html new file mode 100644 index 00000000..fa06f4f1 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/layout.html @@ -0,0 +1,42 @@ + + + + Mithril.js + + + + + + +
+
+ +

Mithril [version]

+ +
+
+
+
+ [body] +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/learning-mithril.html b/dist/archive/v2.0.0-rc.5/learning-mithril.html new file mode 100644 index 00000000..f10872c2 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/learning-mithril.html @@ -0,0 +1,89 @@ + + + + Learning Resources - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Learning Resources

+ +

Links to Mithril learning content:

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/lifecycle-methods.html b/dist/archive/v2.0.0-rc.5/lifecycle-methods.html new file mode 100644 index 00000000..16047081 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/lifecycle-methods.html @@ -0,0 +1,228 @@ + + + + Lifecycle methods - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+
function ComponentWithState() {
+    var initialData
+    return {
+        oninit: function(vnode) {
+            initialData = vnode.attrs.data
+        },
+        view: function(vnode) {
+            return [
+                // displays data from initialization time:
+                m("div", "Initial: " + initialData),
+                // displays current data:
+                m("div", "Current: " + vnode.attrs.data)
+            ]
+        }
+    }
+}
+
+m(ComponentWithState, {data: "Hello"})
+

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.

+

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.

+
function RedrawReporter() {
+    var count = 0
+    return {
+        onupdate: function() {
+            console.log("Redraws so far: ", ++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.

+
function Timer() {
+    var timeout = setTimeout(function() {
+        console.log("timed out")
+    }, 1000)
+
+    return {
+        onremove: function() {
+            clearTimeout(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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/logo.svg b/dist/archive/v2.0.0-rc.5/logo.svg new file mode 100644 index 00000000..ebac4eb3 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/archive/v2.0.0-rc.5/mount.html b/dist/archive/v2.0.0-rc.5/mount.html new file mode 100644 index 00000000..4086a6a6 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/mount.html @@ -0,0 +1,143 @@ + + + + mount(root, component) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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)
+

To pass arguments when mounting a component use:

+
m.mount(element, {view: function () {return m(Component, attrs)}})
+
+

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

+

m.mount(element, Component), when called renders the component into the element and subscribe the (element, Component) pair to the redraw subsystem. That tree will be re-rendered when manual or automatic redraws are triggered.

+

On redraw, the new vDOM tree is compared (or "diffed") with the old one, 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 redraws in response to view events, m.redraw() calls or m.request() calls. Vnodes rendered via m.render() do not.

+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/parsePathname.html b/dist/archive/v2.0.0-rc.5/parsePathname.html new file mode 100644 index 00000000..e7c96485 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/parsePathname.html @@ -0,0 +1,107 @@ + + + + parsePathname(string) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

parsePathname(string)

+ +
+

Description

+

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

+
var object = m.parsePathname("/path/user?a=1&b=2")
+// {path: "/path/user", params: {a: "1", b: "2"}}
+
+

Signature

+

object = m.parsePathname(string)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
stringStringYesA URL
returnsObjectA {path, params} pair where path is the normalized path and params is the parsed parameters.
+

How to read signatures

+
+

How it works

+

The m.parsePathname method creates an object from a path with a possible query string and hash string. It is useful for parsing a URL into more sensible paths, and it's what m.route uses internally to normalize paths to later match them. It uses m.parseQueryString to parse the query parameters into an object.

+
var data = m.parsePathname("/path/user?a=hello&b=world#random=hash&some=value")
+
+// data.path is "/path/user"
+// data.params is {a: "hello", b: "world", random: "hash", some: "value"}
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/parseQueryString.html b/dist/archive/v2.0.0-rc.5/parseQueryString.html new file mode 100644 index 00000000..9c40886e --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/parseQueryString.html @@ -0,0 +1,132 @@ + + + + parseQueryString(string) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, object)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
stringStringYesA querystring
objectObjectNoAn existing key-value map to merge values into, potentially from a previous m.parseQueryString call
returnsObjectA key-value map, object if provided
+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/paths.html b/dist/archive/v2.0.0-rc.5/paths.html new file mode 100644 index 00000000..d15f3f63 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/paths.html @@ -0,0 +1,183 @@ + + + + Path Handling - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Path Handling

+ +
+

m.route, m.request, and m.jsonp each have a concept called a path. This is used to generate the URL you route to or fetch from.

+

Path types

+

There are two general types of paths: raw paths and parameterized paths.

+ +

For m.request and m.jsonp, these can be pretty much any URL, but for routes, these can only be absolute URL path names without schemes or domains.

+

Path parameters

+

Path parameters are themselves pretty simple. They come in two forms:

+ +

You're probably wondering what that params object is supposed to be. It's pretty simple: it's the params in either m.route.set(path, params), m.request({url, params}), or m.jsonp({url, params}).

+

When receiving routes via m.route(root, defaultRoute, routes), you can use these parameters to extract values from routes. They work basically the same way as generating the paths, just in the opposite direction.

+
// Edit a single item
+m.route(document.body, "/edit/1", {
+    "/edit/:id": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing user " + m.route.param("id"))
+            ]
+        }
+    },
+})
+
+// Edit an item identified by path
+m.route(document.body, "/edit/pictures/image.jpg", {
+    "/edit/:file...": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing file " + m.route.param("file"))
+            ]
+        }
+    },
+})
+

In the first example, assuming you're navigating to the default route in each, m.route.param("id") would be read as "1" and m.route.param("file") would be read as pictures/image.jpg.

+

Path parameters may be delimited by either a /, -, or .. This lets you have dynamic path segments, and they're considerably more flexible than just a path name. For example, you could match against routes like "/edit/:name.:ext" for editing based on file extension or "/:lang-:region/view" for a localized route.

+

Path parameters are greedy: given a declared route "/edit/:name.:ext", if you navigate to /edit/file.test.png, the parameters extracted will be {name: "file.test", ext: "png"}, not {name: "file", ext: "test.png"}. Similarly, given "/route/:path.../view/:child...", if you go to /route/foo/view/bar/view/baz, the parameters extracted will be {path: "foo/view/bar", child: "baz"}.

+

Parameter normalization

+

Path parameters that are interpolated into path names are omitted from the query string, for convenience and to keep the path name reasonably readable. For example, this sends a server request of GET /api/user/1/connections?sort=name-asc, omitting the duplicate id=1 in the URL string.

+
m.request({
+    url: "https://example.com/api/user/:userID/connections",
+    params: {
+        userID: 1,
+        sort: "name-asc"
+    }
+})
+

You can also specify parameters explicitly in the query string itself, such as in this, which is equivalent to the above:

+
m.request({
+    url: "https://example.com/api/user/:userID/connections?sort=name-asc",
+    params: {
+        userID: 1
+    }
+})
+

And of course, you can mix and match. This fires a request to GET /api/user/1/connections?sort=name-asc&first=10.

+
m.request({
+    url: "https://example.com/api/user/:userID/connections?sort=name-asc",
+    params: {
+        userID: 1,
+        first: 10
+    }
+})
+

This even extends to route matching: you can match against a route with explicit query strings. It retains the matched parameter for convenience, so you can still access them via vnode parameters or via m.route.param. Note that although this is possible, it's not generally recommended, since you should prefer paths for pages. It could sometimes useful if you need to generate a somewhat different view just for a particular file type, but it still logically is a query-like parameter, not a whole separate page.

+
// Note: this is generally *not* recommended - you should prefer paths for route
+// declarations, not query strings.
+m.route(document.body, "/edit/1", {
+    "/edit?type=image": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing photo")
+            ]
+        }
+    },
+    "/edit": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing " + m.route.param("type"))
+            ]
+        }
+    }
+})
+

Note that query parameters are implicit - you don't need to name them to accept them. You can match based on an existing value, like in "/edit?type=image", but you don't need to use "/edit?type=:type" to accept the value. In fact, Mithril would treat that as you trying to literally match against m.route.param("type") === ":type". Or in summary, use m.route.param("key") to extract parameters - it simplifies things.

+

Path normalization

+

Parsed paths are always returned with all the duplicate parameters and extra slashes dropped, and they always start with a slash. These little differences often get in the way, and it makes routing and path handling a lot more complicated than it should be. Mithril internally normalizes paths for routing, but it does not expose the current, normalized route directly. (You could compute it via m.parsePathname(m.route.get()).path.)

+

When parameters are deduplicated during matching, parameters in the query string are preferred over parameters in the path name, and parameters towards the end of the URL are preferred over parameters closer to the start of the URL.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/promise.html b/dist/archive/v2.0.0-rc.5/promise.html new file mode 100644 index 00000000..66a0ada8 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/promise.html @@ -0,0 +1,439 @@ + + + + Promise(executor) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Promise(executor)

+ +
+

Description

+

A ES6 Promise polyfill.

+

A Promise is a mechanism for working with asynchronous computations.

+

Mithril provides a polyfill when the environment does not support Promises. The polyfill can also be referenced specifically via m.PromisePolyfill.

+
+

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 asynchronous 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/redraw.html b/dist/archive/v2.0.0-rc.5/redraw.html new file mode 100644 index 00000000..4967a992 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/redraw.html @@ -0,0 +1,133 @@ + + + + redraw() - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. The autoredraw system, which is built on top of m.redraw() will take care of it.

+

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

+
+

Signature

+

m.redraw()

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

Static members

+
m.redraw.sync
+

m.redraw.sync()

+ + + + + + + + + + + + + + + +
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.

+

m.redraw() always triggers an asynchronous redraws, whereas m.redraw.sync() triggers a synchronous one. m.redraw() is tied to window.requestAnimationFrame(). It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.

+

m.redraw.sync() is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat:

+ +

m.redraw() doesn't have any of those issues, you can call it from wherever you like.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/releasing.html b/dist/archive/v2.0.0-rc.5/releasing.html new file mode 100644 index 00000000..e2e4876d --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/releasing.html @@ -0,0 +1,201 @@ + + + + Mithril Release Processes - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Mithril Release Processes

+ +

Note These steps all assume that MithrilJS/mithril.js is a git remote named mithriljs, adjust accordingly if that doesn't match your setup.

+ +

Releasing a new Mithril version

+

Prepare the release

+
    +
  1. Ensure your local branch is up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Determine patch level of the change
  2. +
  3. Update information in docs/change-log.md to match reality of the new version being prepared for release
  4. +
  5. Replace all existing references to mithril@next to mithril if moving from a release candidate to stable.
  6. +
  7. Commit changes to next
  8. +
+
$ git add .
+$ git commit -m "Preparing for release"
+
+# Push to your branch
+$ git push
+
+# Push to MithrilJS/mithril.js
+$ git push mithriljs next

Merge from next to master

+
    +
  1. Switch to master and make sure it's up to date
  2. +
+
$ git checkout master
+$ git pull --rebase mithriljs master
+
    +
  1. merge next on top of it
  2. +
+
$ git merge next
+
    +
  1. Clean & update npm dependencies and ensure the tests are passing.
  2. +
+
$ npm prune
+$ npm i
+$ npm test
+

Publish the release

+
    +
  1. npm run release <major|minor|patch|semver>, see the docs for npm version
  2. +
  3. The changes will be automatically pushed to your fork
  4. +
  5. Push the changes to MithrilJS/mithril.js
  6. +
+
$ git push mithriljs master
+
    +
  1. Travis will push the new release to npm & create a GitHub release
  2. +
+

Merge master back into next

+

This helps to ensure that the version field of package.json doesn't get out of date.

+
    +
  1. Switch to next and make sure it's up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Merge master back onto next
  2. +
+
$ git merge master
+
    +
  1. Push the changes to your fork & MithrilJS/mithril.js
  2. +
+
$ git push
+$ git push mithriljs next
+

Update the GitHub release

+
    +
  1. The GitHub Release will require a manual description & title to be added. I suggest coming up with a fun title & then copying the docs/change-log.md entry for the build.
  2. +
+

Updating mithril.js.org

+

Fixes to documentation can land whenever, updates to the site are published via Travis.

+
# These steps assume that MithrilJS/mithril.js is a git remote named "mithriljs"
+
+# Ensure your next branch is up to date
+$ git checkout next
+$ git pull mithriljs next
+
+# Splat the docs folder from next onto master
+$ git checkout master
+$ git checkout next -- ./docs
+
+# Manually ensure that no new feature docs were added
+
+$ git push mithriljs
+

After the Travis build completes the updated docs should appear on https://mithril.js.org in a few minutes.

+

Note: When updating the stable version with a release candidate out, make sure to update the index + navigation to point to the new stable version!!!

+

Releasing a new ospec version

+
    +
  1. Ensure your local branch is up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Determine patch level of the change
  2. +
  3. Update version field in ospec/package.json to match new version being prepared for release
  4. +
  5. Commit changes to next
  6. +
+
$ git add .
+$ git commit -m "chore(ospec): ospec@<version>"
+
+# Push to your branch
+$ git push
+
+# Push to MithrilJS/mithril.js
+$ git push mithriljs next

Merge from next to master

+
    +
  1. Switch to master and make sure it's up to date
  2. +
+
$ git checkout master
+$ git pull --rebase mithriljs master
+
    +
  1. merge next on top of it
  2. +
+
$ git checkout next -- ./ospec
+$ git add .
+$ git commit -m "chore(ospec): ospec@<version>"
+
    +
  1. Ensure the tests are passing!
  2. +
+

Publish the release

+
    +
  1. Push the changes to MithrilJS/mithril.js
  2. +
+
$ git push mithriljs master
+
    +
  1. Publish the changes to npm from the /ospec folder. That bit is important to ensure you don't accidentally ship a new Mithril release!
  2. +
+
$ cd ./ospec
+$ npm publish
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/render.html b/dist/archive/v2.0.0-rc.5/render.html new file mode 100644 index 00000000..fd20cd09 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/render.html @@ -0,0 +1,133 @@ + + + + render(element, vnodes) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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-sufficient. 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/request.html b/dist/archive/v2.0.0-rc.5/request.html new file mode 100644 index 00000000..03c55bb8 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/request.html @@ -0,0 +1,581 @@ + + + + request(options) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
optionsObjectYesThe request options to pass.
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 path name to send the request to, optionally interpolated with values from options.data.
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.timeoutNumberNoThe amount of milliseconds a request can take before automatically being terminated. Defaults to undefined.
options.responseTypeStringNoThe expected type of the response. Defaults to "" if extract is defined, "json" if missing. If responseType: "json", it internally performs JSON.parse(responseText).
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(any)NoA deserialization method to be applied to the xhr.response or normalized xhr.responseText. Defaults to the identity function. If extract is defined, deserialize will be skipped.
options.extractany = Function(xhr, options)NoA hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns options.deserialize(parsedResponse), throwing an exception when the server response status code indicates an error or when the response is syntactically invalid. If a custom extract callback is provided, the xhr parameter is the XMLHttpRequest instance used for the request, and options is the object that was passed to the m.request call. Additionally, deserialize will be skipped and the value returned from the extract callback will be left as-is when the promise resolves.
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
+

promise = m.request(url, options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringYesThe path name to send the request to. options.url overrides this when present.
optionsObjectNoThe request options to pass.
returnsPromiseA promise that resolves to the response data, after it has been piped through the extract, deserialize and type methods
+

This second form is mostly equivalent to m.request(Object.assign({url: url}, options)), just it does not depend on the ES6 global Object.assign internally.

+

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 returns a promise and triggers 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).

+

If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection.

+
+

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.

+
+

Error handling

+

When a non-file: request returns with any status other than 2xx or 304, it rejects with an error. This error is a normal Error instance, but with a few special properties.

+ +

This is useful in many cases where errors are themselves things you can account for. If you want to detect if a session expired - you can do if (error.code === 401) return promptForAuth().then(retry). If you hit an API's throttling mechanism and it returned an error with a "timeout": 1000, you could do a setTimeout(retry, error.response.timeout).

+
+

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, files[i])
+    }
+
+    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.upload.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 xhr.responseText as JSON and returns the parsed object. It may be useful to inspect a server response in more detail and process it manually. 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:

+ +

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:

+ +

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/route.html b/dist/archive/v2.0.0-rc.5/route.html new file mode 100644 index 00000000..906a248d --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/route.html @@ -0,0 +1,779 @@ + + + + route(root, defaultRoute, routes) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. Triggers an asynchronous redraw off all mount points.

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
pathStringYesThe path name to route to, without a prefix. The path may include parameters, interpolated with values from data.
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
+

Remember that when using .set with params you also need to define the route:

+
var Article = {
+    view: function(vnode) {
+        return "This is article " + vnode.attrs.articleid
+    }
+}
+
+m.route(document.body, {
+    '/article/:articleid': Article
+})
+m.route.set('/article/:articleid', {articleid: 1})
+
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
+ +

This function can be used as the oncreate (and onupdate) hook in a m("a") vnode:

+
m("a[href=/]", {oncreate: m.route.link})
+

Using m.route.link as a oncreate hook causes the link to behave as a router link (i.e. it navigates to the route specified in href, instead of navigating away from the current page to the URL specified in href.

+

If the href attribute is not static, the onupdate hook must also be set:

+
m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link})
+

m.route.link can also set the options passed to m.route.set when the link is clicked by calling the function in the lifecycle methods:

+
m("a[href=/]", {oncreate: m.route.link({replace: true})})
+

m.route.link(args)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
argsVnode|ObjectYesThis method is meant to be used as or in conjunction with an <a> vnode's oncreate and onupdate hooks
returnsfunctionReturns the onclick handler function for the component
+
m.route.param
+

Retrieves a route parameter from the last fully resolved route. A route parameter is a key-value pair. Route parameters may come from a few different places:

+ +

value = m.route.param(key)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
keyStringNoA route parameter name (e.g. id in route /users/:id, or page in path /users/1?page=3, or a key in history.state)
returnsString|ObjectReturns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys
+

Note that in the onmatch function of a RouteResolver, the new route hasn't yet been fully resolved, and m.route.param() will return the parameters of the previous route, if any. onmatch receives the parameters of the new route as an argument.

+

RouteResolver

+

A RouteResolver is a non-component object that contains an onmatch method and/or a render method. Both methods are optional, but at least one must be present.

+

If an object can be detected as a component (by the presence of a view method or by being a function/class), it will be treated as such even if it has onmatch or render methods. Since a RouteResolver is not a component, 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, route)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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.
routeStringThe router path requested by the last routing action, excluding interpolated routing parameter values
returnsComponent|Promise|undefinedReturns 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
returnsArray<Vnode>|VnodeThe vnodes to be rendered
+
+

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 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 hash strategy is guaranteed to work in browsers that don't support history.pushState, because it can fall back to using onhashchange. Use this strategy if you want to keep the hashes purely local.

+

The querystring strategy allows server-side detection, but it doesn't appear as a normal path. Use this strategy if you want to support and potentially detect anchored links server-side and you are not able to make the changes necessary to support the pathname strategy (like if you're using Apache and can't modify your .htaccess).

+

The pathname strategy produces the cleanest looking URLs, but 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.

+

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.

+

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", {
+    "/edit/:file...": Edit,
+})
+

Handling 404s

+

For isomorphic / universal JavaScript app, an url param and a variadic route combined is very useful to display custom 404 error page.

+

In a case of 404 Not Found error, the server send back the custom page to client. When Mithril is loaded, it will redirect client to the default route because it can't know that route.

+
m.route(document.body, "/", {
+  "/": homeComponent,
+  // [...]
+  "/:404...": errorPageComponent
+});
+

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: function (e) { state.term = e.target.value },
+                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, route) {
+            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 needed.

+

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 has oninit and oncreate lifecycle methods, they would only fire 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 RouteResolver's onmatch hook can be used to run logic before the top level component in a route is initialized. 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: function (e) { Auth.setUsername(e.target.value) },
+                value: Auth.username
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { Auth.setPassword(e.target.value) },
+                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. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that loadUsers() returns a Promise, but any Promise returned by oninit is currently ignored. The second render pass comes from the background option for m.request.

+
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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/signatures.html b/dist/archive/v2.0.0-rc.5/signatures.html new file mode 100644 index 00000000..2b0e7f0d --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/signatures.html @@ -0,0 +1,126 @@ + + + + How to read signatures - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/simple-application.html b/dist/archive/v2.0.0-rc.5/simple-application.html new file mode 100644 index 00000000..676274fa --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/simple-application.html @@ -0,0 +1,535 @@ + + + + Simple application - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Simple application

+ +

Let's develop a simple application that covers some of the major aspects of Single Page Applications

+

An interactive running example can be seen here flems: Simple Application

+

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 https://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: "https://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:

+
// src/views/UserList.js
+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 deviates 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=button]", "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: "https://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: "https://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: "https://rem-rest-api.herokuapp.com/api/users/" + 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=button]", "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", {
+                onsubmit: function(e) {
+                    e.preventDefault()
+                    User.save()
+                }
+            }, [
+            m("label.label", "First name"),
+            m("input.input[type=text][placeholder=First name]", {
+                oninput: function (e) {User.current.firstName = e.target.value},
+                value: User.current.firstName
+            }),
+            m("label.label", "Last name"),
+            m("input.input[placeholder=Last name]", {
+                oninput: function (e) {User.current.lastName = e.target.value},
+                value: User.current.lastName
+            }),
+            m("button.button[type=submit]", "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: "https://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: "https://rem-rest-api.herokuapp.com/api/users/" + id,
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.current = result
+        })
+    },
+
+    save: function() {
+        return m.request({
+            method: "PUT",
+            url: "https://rem-rest-api.herokuapp.com/api/users/" + User.current.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:

+
// 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.id} />.

+

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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/stream.html b/dist/archive/v2.0.0-rc.5/stream.html new file mode 100644 index 00000000..05e5dcc6 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/stream.html @@ -0,0 +1,704 @@ + + + + stream() - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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")
+

You can also download the module directly if your environment does not support a bundling toolchain:

+
<script src="https://unpkg.com/mithril@next/stream/stream.js"></script>
+

When loaded directly with a <script> tag (rather than required), the stream library will be exposed as window.m.stream. If window.m is already defined (e.g. because you also use the main Mithril script), it will attach itself to the existing object. Otherwise it creates a new window.m. If you want to use streams in conjunction with Mithril as raw script tags, you should include Mithril in your page before mithril/stream, because mithril will otherwise overwrite the window.m object defined by mithril/stream. This is not a concern when the libraries are consumed as CommonJS modules (using require(...)).

+
+

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.scan
+

Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value.

+

Note that you can prevent dependent streams from being updated by returning the special value stream.SKIP inside the accumulator function.

+

stream = Stream.scan(fn, accumulator, stream)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
fn(accumulator, value) -> result | SKIPYesA function that takes an accumulator and value parameter and returns a new accumulator value of the same type
accumulatoranyYesThe starting value for the accumulator
streamStreamYesStream containing the values
returnsStreamReturns a new stream containing the result
+

How to read signatures

+
+
Stream.scanMerge
+

Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream.

+

stream = Stream.scanMerge(pairs, accumulator)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
pairsArray<[Stream, (accumulator, value) -> value]>YesAn array of tuples of stream and scan functions
accumulatoranyYesThe starting value for the accumulator
returnsStreamReturns a new stream containing the result
+

How to read signatures

+
+
Stream.lift
+

Creates a computed stream that reactively updates if any of its upstreams are updated. See combining streams. Unlike combine, the input streams are a variable number of arguments (instead of an array) and the callback receives the stream values instead of streams. There is no changed parameter. This is generally a more user-friendly function for applications than combine.

+

stream = Stream.lift(lifter, stream1, stream2, ...)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
lifter(any...) -> anyYesSee lifter argument
streams...list of StreamsYesStreams to be lifted
returnsStreamReturns a stream
+

How to read signatures

+
+
lifter
+

Specifies how the value of a computed stream is generated. See combining streams

+

any = lifter(streams...)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
streams...splat of StreamsNoSplat of zero or more streams that correspond to the streams passed to stream.lift
returnsanyReturns a computed value
+

How to read signatures

+
+
Stream.SKIP
+

A special value that can be returned to stream callbacks to skip 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/map"](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 event callbacks and similar.

+
// a stream
+var user = stream("")
+
+// a bi-directional binding to the stream
+m("input", {
+    oninput: function (e) { user(e.target.value) },
+    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.SKIP

+
var skipped = stream(1).map(function(value) {
+    return stream.SKIP
+})
+
+skipped.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"
+

Or you can use the helper function stream.lift()

+
var a = stream("hello")
+var b = stream("world")
+
+var greeting = stream.lift(function(_a, _b) {
+    return _a + " " + _b
+}, a, b)
+
+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 brings the performance benefits of not recomputing downstreams unnecessarily.

+

You can prevent dependent streams from being updated by returning the special value stream.SKIP

+
var skipped = stream.combine(function(stream) {
+    return stream.SKIP
+}, [stream(1)])
+
+skipped.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 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/style.css b/dist/archive/v2.0.0-rc.5/style.css new file mode 100644 index 00000000..60b3a9a3 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/style.css @@ -0,0 +1,97 @@ +body {background:white;-webkit-text-size-adjust: 100%;} +body,table,h5 {font-weight:normal;font-size:16px;font-family:'Open Sans',sans-serif;} +header,main {margin:auto;max-width:1000px;} +header section {position:absolute;width:250px;} +nav a {border-left:1px solid #ddd;padding:0 10px;} +nav a:first-child {border:0;padding-left:0;} +main {margin-bottom:100px;} +main section {margin-left:270px;} +p {margin:0 0 15px;} +pre,code {background:#eee;font-family:monospace;font-size:14px;} +pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;margin:20px 0;} +code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:5px 3px;white-space:pre;} +pre code {border:0;margin:0;padding:0;} +table {border-collapse:collapse;margin:0 0 30px;width:100%;} +tbody tr:nth-child(odd) {background:#fafafa;} +thead tr,tbody tr:nth-child(even) {background:#f3f3f3;} +tr {border-bottom:1px solid #eee;} +th {text-align:left;} +th,td {padding:3px 10px;vertical-align:top;} +a {color:#1e5799;text-decoration:none;} +a:hover {text-decoration:underline;} +hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;} + +/* Headings */ +h1,h2,h3,h4,h5 {position:relative} +h1 {font-size:24px;margin:0 0 15px;} +h2 {font-size:22px;margin:45px 0 15px;} +h3 {font-size:20px;margin:45px 0 15px;} +h4 {font-size:18px;margin:30px 0 15px;} +h5 {font-weight:bold;margin:15px 0 15px;} +h1 img {transform:rotate(180deg);vertical-align:middle;width:20px;} +h1 small {font-size:16px;} +h2 a,h3 a,h4 a,h5 a, +h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover, +h2 a:active,h3 a:active,h4 a:active,h5 a:active, +h2 a:visited,h3 a:visited,h4 a:visited,h5 a:visited {color:#000;text-decoration:none;} +h2::before,h3::before,h4::before,h5::before {content:"#";position:absolute;left:-20px;visibility:hidden;} +h2:hover::before,h3:hover::before,h4:hover::before,h5:hover::before {visibility:visible;} +#signature + p code {padding:3px 10px;} +h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;z-index:1;} +h1 + ul + hr {display:none;} +h1 + ul li {list-style:none;margin:0;padding:0;} +h1 + ul li:last-child {border-bottom:0;} +h1 + ul ul {margin:0 0 2px;padding:0 0 0 15px;} +h1 + ul ul li {border:0;} +h1 + ul strong + ul {border-left:3px solid #1e5799;} + +.hamburger {display:none;} + +@keyframes grow { + from {transform:scaleX(0)} + to {transform:scaleX(100%)} +} + +@media (max-width: 767px) { + .hamburger {display:block;font-size:30px;padding:0 10px;position:fixed;right:0;top:0;z-index:2;} + .hamburger:hover {text-decoration:none;} + main section {margin:0;} + header section {margin:0 0 20px;position:static;width:auto;} + h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;z-index:1} + h1 + ul + hr {display:block;} + .navigating h1 + ul {display:block;} + .navigating {overflow:hidden;} +} +@media (max-width: 1024px) { + table,table tbody,table tr,table th,table td {display:block;} + table thead {display:none;} + table td:before {display:inline-block;font-style:italic;font-weight:bold;padding:0 10px 0 0;width:100px;} + table tr:not(:last-child) td:nth-child(1):before {content:"Argument:";} + table tr:last-child td:nth-child(3) {display:none;} + table td:nth-child(2):before {content:"Type:";} + table td:nth-child(3):before {content:"Required:";} + table td:nth-child(4):before {content:"Description:";} + #structure ~ table td:nth-child(1):before {content:"Property:";} + #structure ~ table td:nth-child(2):before {content:"Type:";} + #structure ~ table td:nth-child(3):before {content:"Description:";} + #vnode-types ~ table td:nth-child(1):before {content:"Vnode type:";} + #vnode-types ~ table td:nth-child(2):before {content:"Example:";} + #vnode-types ~ table td:nth-child(3):before {content:"Description:";} + #lifecycle-methods ~ table td:nth-child(1):before {content:"Hook:";} + #lifecycle-methods ~ table td:nth-child(2):before {content:"Description:";} + #react ~ table td:nth-child(1):before {content:"React:";} + #angular ~ table td:nth-child(1):before {content:"Angular:";} + #vue ~ table td:nth-child(1):before {content:"Vue:";} + #comparisons ~ table td:nth-child(2):before {content:"Mithril:";} +} +@media print { + nav,h1 + ul {display:none;} + main section {margin:0;} +} + +/* prism theming */ +.token.comment,.token.prolog,.token.doctype,.token.cdata {color:#888;} +.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol {color:#905;} +.token.selector,.token.attr-name,.token.string,.token.builtin {color:#690;} +.token.atrule,.token.attr-value,.token.punctuation,.token.keyword {color:#1e5799;} +.token.regex,.token.important {color:#e90;} diff --git a/dist/archive/v2.0.0-rc.5/support.html b/dist/archive/v2.0.0-rc.5/support.html new file mode 100644 index 00000000..7adcc4fb --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/support.html @@ -0,0 +1,86 @@ + + + + Getting Help - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Getting Help

+ +

Mithril has an active & welcoming community on Gitter, or feel free to ask questions on Stack Overflow using the mithril.js tag.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/testing.html b/dist/archive/v2.0.0-rc.5/testing.html new file mode 100644 index 00000000..13bd9fb2 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/testing.html @@ -0,0 +1,145 @@ + + + + Testing - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+

Running mithril in a non-browser environment

+

Mithril has a few dependencies on globals that exist in all its supported browser environments but are missing in all non-browser environments. To work around this you can use the browser mocks that ship with the mithril npm package.

+

The simplest way to do this is ensure the following snippet of code runs before you include mithril itself in your project.

+
// Polyfill DOM env for mithril
+global.window = require("mithril/test-utils/browserMock.js")();
+global.document = window.document;
+

Once that snippet has been run you can require("mithril") and it should be quite happy.

+
+

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 inadvertently 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", 
+            m("p", "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("p")
+        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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/trust.html b/dist/archive/v2.0.0-rc.5/trust.html new file mode 100644 index 00000000..ccf55e37 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/trust.html @@ -0,0 +1,213 @@ + + + + trust(html) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

trust(html)

+ +
+

Description

+

Turns an HTML or SVG string into unescaped HTML or SVG. 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 or SVG 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. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/version.html b/dist/archive/v2.0.0-rc.5/version.html new file mode 100644 index 00000000..3138573b --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/version.html @@ -0,0 +1,101 @@ + + + + version - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/archive/v2.0.0-rc.5/vnodes.html b/dist/archive/v2.0.0-rc.5/vnodes.html new file mode 100644 index 00000000..08a2aaa1 --- /dev/null +++ b/dist/archive/v2.0.0-rc.5/vnodes.html @@ -0,0 +1,237 @@ + + + + Virtual DOM nodes - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 as 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 a declarative immediate mode API, a style of rendering that makes it drastically easier to manage UI complexity.

+

To illustrate why immediate mode is so important, consider the DOM API and HTML. The DOM API is an imperative retained mode API and requires 1. writing out exact instructions to assemble a DOM tree procedurally, and 2. writing out other instructions to update that tree. 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 of introducing bugs and more chances to make code harder to understand.

+

In contrast, HTML is closer to an immediate 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 are created via the m() hyperscript utility:

+
m("div", {id: "test"}, "hello")
+

Hyperscript 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 their only child.
domElement?Points to the element that corresponds to the vnode. This property is undefined in the oninit lifecycle method. In fragments 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).
stateObject?An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the state inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure.
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 or modify it.
instanceObject?For components, a storage location for the value returned by the view. This property is only used internally by Mithril, do not use or modify it.
skipBooleanThis property is only used internally by Mithril when diffing keyed lists, do not use or modify 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. +
+
+ + + + + + diff --git a/dist/autoredraw.html b/dist/autoredraw.html new file mode 100644 index 00000000..ffa1b2a1 --- /dev/null +++ b/dist/autoredraw.html @@ -0,0 +1,168 @@ + + + + The auto-redraw system - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, raw Promise resolutions 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.

+
function StableComponent() {
+    var height = 0
+
+    return {
+        oncreate: function(vnode) {
+            height = vnode.dom.offsetHeight
+            m.redraw()
+        },
+        view: function() {
+            return m("div", "This component is " + 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. +
+
+ + + + + + diff --git a/dist/buildPathname.html b/dist/buildPathname.html new file mode 100644 index 00000000..12497336 --- /dev/null +++ b/dist/buildPathname.html @@ -0,0 +1,106 @@ + + + + buildPathname(object) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

buildPathname(object)

+ +
+

Description

+

Turns a path template and a parameters object into a string of form /path/user?a=1&b=2

+
var querystring = m.buildPathname("/path/:id", {id: "user", a: "1", b: "2"})
+// "/path/user?a=1&b=2"
+
+

Signature

+

querystring = m.buildPathname(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.buildPathname creates a path name from a path template and a parameters object. It's useful for building URLs, and it's what m.route, m.request, and m.jsonp all use internally to interpolate paths. It uses m.buildQueryString to generate the query parameters to append to the path name.

+
var querystring = m.buildPathname("/path/:id", {id: "user", a: 1, b: 2})
+
+// querystring is "/path/user?a=1&b=2"
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/buildQueryString.html b/dist/buildQueryString.html new file mode 100644 index 00000000..bf5c5932 --- /dev/null +++ b/dist/buildQueryString.html @@ -0,0 +1,116 @@ + + + + buildQueryString(object) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. +
+
+ + + + + + diff --git a/dist/change-log.html b/dist/change-log.html new file mode 100644 index 00000000..4f6f5124 --- /dev/null +++ b/dist/change-log.html @@ -0,0 +1,739 @@ + + + + Change log - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Change log

+ +
+

Upcoming...

+

v2.0.0-rc

+

Breaking changes

+ +

News

+ +

Bug fixes

+ +
+

v1.2.0

+

News

+ +

Bug fixes

+ +

Note

+ +
+

v1.1.6

+

Bug fixes

+ +

Ospec improvements

+ +
+

v1.1.5

+

Bug fixes

+ +
+

v1.1.4

+

Bug fixes

+ +

Ospec improvements:

+ +
+

v1.1.3

+

Bug fixes

+ +
+

v1.1.2

+

Bug fixes

+ +
+

Docs / Repo maintenance

+

Our thanks to @0joshuaolson1, @ACXgit, @cavemansspa, @CreaturesInUnitards, @dlepaux, @isaaclyman, @kevinkace, @micellius, @spacejack and @yurivish

+

Other

+ +
+

v1.1.1

+

Bug fixes

+ +
+

v1.1.0

+

News

+ +

Bug fixes

+ +
+

v1.0.1

+

News

+ +

Bug fixes

+ +
+

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"
+        }
+    }
+})
+
+

Building/Parsing query strings

+

v0.2.x used methods hanging off of m.route, m.route.buildQueryString() and m.route.parseQueryString(). In v1.x these have been broken out and attached to the root m.

+

v0.2.x

+
var qs = m.route.buildQueryString({ a : 1 });
+
+var obj = m.route.parseQueryString("a=1");
+

v1.x

+
var qs = m.buildQueryString({ a : 1 });
+
+var obj = m.parseQueryString("a=1");
+
+

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("/")}})
+    }
+}
+
+

Run code on component removal

+

Components no longer call this.onunload when they are being removed. They now use the standardized lifecycle hook onremove.

+

v0.2.x

+
var Component = {
+    controller: function() {
+        this.onunload = function(e) {
+            // ...
+        }
+    },
+    view: function() {
+        // ...
+    }
+}
+

v1.x

+
var Component = {
+    onremove : function() {
+        // ...
+    }
+    view: function() {
+        // ...
+    }
+}
+
+

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 = function() {
+    return 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. +
+
+ + + + + + diff --git a/dist/code-of-conduct.html b/dist/code-of-conduct.html new file mode 100644 index 00000000..419b659a --- /dev/null +++ b/dist/code-of-conduct.html @@ -0,0 +1,142 @@ + + + + Contributor Covenant Code of Conduct - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Contributor Covenant Code of Conduct

+ +

Our Pledge

+

In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation.

+

Our Standards

+

Examples of behavior that contributes to creating a positive environment +include:

+ +

Examples of unacceptable behavior by participants include:

+ +

Our Responsibilities

+

Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior.

+

Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful.

+

Scope

+

This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers.

+

Enforcement

+

Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at github@patcavit.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately.

+

Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership.

+

Attribution

+

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at http://contributor-covenant.org/version/1/4

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/components.html b/dist/components.html new file mode 100644 index 00000000..3c0a5a5f --- /dev/null +++ b/dist/components.html @@ -0,0 +1,597 @@ + + + + Components - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

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

Lifecycle methods

+

Components can have the same lifecycle methods as virtual DOM nodes. Note that vnode is passed as an argument to each lifecycle method, as well as to view (with the previous vnode passed additionally to onbeforeupdate):

+
var ComponentWithHooks = {
+    oninit: function(vnode) {
+        console.log("initialized")
+    },
+    oncreate: function(vnode) {
+        console.log("DOM created")
+    },
+    onbeforeupdate: function(newVnode, oldVnode) {
+        return true
+    },
+    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")
+    },
+    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(vnode) {
+    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.

+
+

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 defined in the attrs object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril itself. Use them in attrs only when you specifically wish to use them as lifecycle methods.

+
+

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.

+

Note that unlike many other frameworks, mutating component state does not trigger redraws or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by m.request complete or when the browser navigates to different routes. Mithril's component state mechanisms simply exist as a convenience for applications.

+

If a state change occurs that is not as a result of any of the above conditions (e.g. after a setTimeout), then you can use m.redraw() to trigger a redraw manually.

+

Closure Component State

+

In the above examples, each component is defined as a POJO (Plain Old JavaScript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a closure component, which is simply a wrapper function which returns a POJO component instance, which in turn carries its own, closed-over scope.

+

With a closure component, state can simply be maintained by variables that are declared within the outer function:

+
function ComponentWithState(initialVnode) {
+    // Component state variable, unique to each instance
+    var count = 0
+
+    // POJO component instance: any object with a
+    // view function which returns a vnode
+    return {
+        oninit: function(vnode){
+            console.log("init a closure component")
+        },
+        view: function(vnode) {
+            return m("div",
+                m("p", "Count: " + count),
+                m("button", {
+                    onclick: function() {
+                        count += 1
+                    }
+                }, "Increment count")
+            )
+        }
+    }
+}
+

Any functions declared within the closure also have access to its state variables.

+
function ComponentWithState(initialVnode) {
+    var count = 0
+
+    function increment() {
+        count += 1
+    }
+
+    function decrement() {
+        count -= 1
+    }
+
+    return {
+        view: function(vnode) {
+            return m("div",
+                m("p", "Count: " + count),
+                m("button", {
+                    onclick: increment
+                }, "Increment"),
+                m("button", {
+                    onclick: decrement
+                }, "Decrement")
+            )
+        }
+    }
+}
+

Closure components are consumed in the same way as POJOs, e.g. m(ComponentWithState, { passedData: ... }).

+

A big advantage of closure components is that we don't need to worry about binding this when attaching event handler callbacks. In fact this is never used at all and we never have to think about this context ambiguities.

+
+

POJO Component State

+

It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via vnode.state and via the this keyword in component methods.

+

At initialization

+

For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of vnode.state. This allows simple "blueprint" state initialization.

+

In the example below, data becomes a property of the ComponentWithInitialState component's vnode.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

+

As you can see, 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.

+
+

ES6 classes

+

If it suits your needs (like in object-oriented projects), components can also be written using ES6 class syntax:

+
class ES6ClassComponent {
+    constructor(vnode) {
+        this.kind = "ES6 class"
+    }
+    view() {
+        return m("div", `Hello from an ${this.kind}`)
+    }
+    oncreate() {
+        console.log(`A ${this.kind} component was created`)
+    }
+}
+

Component classes must define a view() method, detected via .prototype.view, to get the tree to render.

+

They can be consumed in the same way regular components can.

+
// EXAMPLE: via m.render
+m.render(document.body, m(ES6ClassComponent))
+
+// EXAMPLE: via m.mount
+m.mount(document.body, ES6ClassComponent)
+
+// EXAMPLE: via m.route
+m.route(document.body, "/", {
+    "/": ES6ClassComponent
+})
+
+// EXAMPLE: component composition
+class AnotherES6ClassComponent {
+    view() {
+        return m("main", [
+            m(ES6ClassComponent)
+        ])
+    }
+}
+

Class Component State

+

With classes, state can be managed by class instance properties and methods, and accessed via this:

+
class ComponentWithState {
+    constructor(vnode) {
+        this.count = 0
+    }
+    increment() {
+        this.count += 1
+    }
+    decrement() {
+        this.count -= 1
+    }
+    view() {
+        return m("div",
+            m("p", "Count: " + count),
+            m("button", {
+                onclick: () => {this.increment()}
+            }, "Increment"),
+            m("button", {
+                onclick: () => {this.decrement()}
+            }, "Decrement")
+        )
+    }
+}
+

Note that we must wrap the event callbacks in arrow functions so that the this context is preserved correctly.

+
+

Mixing component kinds

+

Components can be freely mixed. A class component can have closure or POJO components as children, etc...

+
+

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: function (e) { this.setUsername(e.target.value) },
+                value: this.username,
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { this.setPassword(e.target.value) },
+                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 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: function (e) { Auth.setUsername(e.target.value) },
+                value: Auth.username
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { Auth.setPassword(e.target.value) },
+                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.

+

Don't forward vnode.attrs itself to other vnodes

+

Sometimes, you might want to keep an interface flexible and your implementation simpler by forwarding attributes to a particular child component or element, in this case Bootstrap's modal. It might be tempting to forward a vnode's attributes like this:

+
// AVOID
+var Modal = {
+    // ...
+    view: function(vnode) {
+        return m(".modal[tabindex=-1][role=dialog]", vnode.attrs, [
+            //         forwarding `vnode.attrs` here ^
+            // ...
+        ])
+    }
+}
+

If you do it like above, you could run into issues when using it:

+
var MyModal = {
+    view: function() {
+        return m(Modal, {
+            // This toggles it twice, so it doesn't show
+            onupdate: function(vnode) {
+                if (toggle) $(vnode.dom).modal("toggle")
+            }
+        }, [
+            // ...
+        ])
+    }
+}
+

Instead, you should forward single attributes into vnodes:

+
// PREFER
+var Modal = {
+    // ...
+    view: function(vnode) {
+        return m(".modal[tabindex=-1][role=dialog]", vnode.attrs.attrs, [
+            //              forwarding `attrs:` here ^
+            // ...
+        ])
+    }
+}
+
+// Example
+var MyModal = {
+    view: function() {
+        return m(Modal, {
+            attrs: {
+                // This toggles it once
+                onupdate: function(vnode) {
+                    if (toggle) $(vnode.dom).modal("toggle")
+                }
+            },
+            // ...
+        })
+    }
+}
+

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. +
+
+ + + + + + diff --git a/dist/contributing.html b/dist/contributing.html new file mode 100644 index 00000000..f49a478d --- /dev/null +++ b/dist/contributing.html @@ -0,0 +1,148 @@ + + + + Contributing FAQs - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Contributing FAQs

+ +

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:

+ +

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 or Bublé? Would a PR to upgrade be welcome?

+

Being able to run Mithril's raw source code in all supported browsers is a requirement for all browser-related modules in this repo. In addition, transpiled code is generally much 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. Besides, we aren't the only one who've decided to drop the semicolon. (We don't use Standard, though.)

+

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.

+

Do you all accept donations?

+

Yes, we do, over at our OpenCollective page. We don't actively seek donations, but they are very much appreciated and are used to support development and related expenses. Both one-time and recurring donations are accepted.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/credits.html b/dist/credits.html new file mode 100644 index 00000000..83530afe --- /dev/null +++ b/dist/credits.html @@ -0,0 +1,109 @@ + + + + Credits - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

+ +

Other people who also deserve recognition:

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/css.html b/dist/css.html new file mode 100644 index 00000000..9b453555 --- /dev/null +++ b/dist/css.html @@ -0,0 +1,151 @@ + + + + CSS - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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:

+ +
+

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 are 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 cryptic class names).

+

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

+

If you are adamant about using a CSS-in-JS library, consider using J2C, which works without configuration and implements @keyframes and @font-face.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/es6.html b/dist/es6.html new file mode 100644 index 00000000..23febcb1 --- /dev/null +++ b/dist/es6.html @@ -0,0 +1,167 @@ + + + + ES6 - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 an 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

+
const path = require('path')
+
+module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: path.resolve(__dirname, './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. +
+
+ + + + + + diff --git a/dist/examples.html b/dist/examples.html new file mode 100644 index 00000000..bf48ab26 --- /dev/null +++ b/dist/examples.html @@ -0,0 +1,95 @@ + + + + Examples - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Examples

+ +

Here are some examples of Mithril in action

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/favicon.ico b/dist/favicon.ico new file mode 100644 index 00000000..61807fae Binary files /dev/null and b/dist/favicon.ico differ diff --git a/dist/favicon.png b/dist/favicon.png new file mode 100644 index 00000000..50712630 Binary files /dev/null and b/dist/favicon.png differ diff --git a/dist/fragment.html b/dist/fragment.html new file mode 100644 index 00000000..c5ef9e15 --- /dev/null +++ b/dist/fragment.html @@ -0,0 +1,142 @@ + + + + fragment(attrs, children) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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
attrsObjectNoHTML attributes or element properties
childrenArray<Vnode>|String|Number|BooleanNoChild vnodes. Can be written as splat arguments
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 and don't require keyed logic, you should use m() instead.

+

Normally you can use simple arrays or splats 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. +
+
+ + + + + + diff --git a/dist/framework-comparison.html b/dist/framework-comparison.html new file mode 100644 index 00000000..174dcd31 --- /dev/null +++ b/dist/framework-comparison.html @@ -0,0 +1,264 @@ + + + + Framework comparison - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. This 10 minute 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.

+ +

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 encouraged 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:

+ +

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:

+ +

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 a Vue 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. All component creation styles in Mithril output the same vnode structure using native JavaScript features only. The direct consequence of leaning on the language is less tooling and a simpler project setup.

+

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. +
+
+ + + + + + diff --git a/dist/hyperscript.html b/dist/hyperscript.html new file mode 100644 index 00000000..e4b2fc6c --- /dev/null +++ b/dist/hyperscript.html @@ -0,0 +1,487 @@ + + + + m(selector, attributes, children) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, attrs, children)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
selectorString|ObjectYesA CSS selector or a component
attrsObjectNoHTML 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 attrs 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>
+

Attributes passed as the second argument

+

You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details).

+
m("button", {
+    class: "my-button",
+    onclick: function() {/* ... */},
+    oncreate: function() {/* ... */}
+})
+

If the value of such an attribute is null or undefined, it is treated as if the attribute was absent.

+

If there are class names in both first and second arguments of m(), they are merged together as you would expect. If the value of the class in the second argument is nullor undefined, it is ignored.

+

If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is null or undefined.

+
+

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]")
+

This even includes custom elements. For example, you can use A-Frame within Mithril, no problem!

+
m("a-scene", [
+    m("a-box", {
+        position: "-1 0.5 -3",
+        rotation: "0 45 0",
+        color: "#4CC3D9",
+    }),
+
+    m("a-sphere", {
+        position: "0 1.25 -5",
+        radius: "1.25",
+        color: "#EF2D5E",
+    }),
+
+    m("a-cylinder", {
+        position: "1 0.75 -3",
+        radius: "0.5",
+        height: "1.5",
+        color: "#FFC65D",
+    }),
+
+    m("a-plane", {
+        position: "0 0 -4",
+        rotation: "-90 0 0",
+        width: "4",
+        height: "4",
+        color: "#7BC8A4",
+    }),
+
+    m("a-sky", {
+        color: "#ECECEC",
+    }),
+])
+

And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using Brick's brick-deck as an example, they have a selected-index attribute with a corresponding selectedIndex getter/setter property.

+
m("brick-deck[selected-index=0]", [/* ... */]) // lowercase
+m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase
+// I know these look odd, but `brick-deck`'s `selectedIndex` property is a
+// string, not a number.
+m("brick-deck", {"selected-index": "0"}, [/* ... */])
+m("brick-deck", {"selectedIndex": "0"}, [/* ... */])
+

For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element my-special-element that has an elem.whitelist array getter/setter property, you could do this, and it'd work as you'd expect:

+
m("my-special-element", {
+    whitelist: [
+        "https://example.com",
+        "http://neverssl.com",
+        "https://google.com",
+    ],
+})
+

If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example:

+
// These two are equivalent
+m("a-entity#player")
+m("a-entity", {id: "player"})
+

Do note that all the properties with magic semantics, like lifecycle attributes, onevent handlers, keys, class, and style, those are still treated the same way they are for normal HTML elements.

+
+

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.

+

You can use both hyphenated CSS property names (like background-color) and camel cased DOM style property names (like backgroundColor). You can also define CSS custom properties, if your browser supports them.

+

Mithril does not attempt to add units to number values. It simply stringifies them.

+
+

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

Mithril accepts functions and EventListener objects. So this will also work:

+
var clickListener = {
+    handleEvent: function(e) {
+        console.log(e)
+    }
+}
+
+m("div", {onclick: clickListener})
+

By default, when an event attached with hyperscript fires, this will trigger Mithril's auto-redraw after your event callback returns (assuming you are using m.mount or m.route instead of m.render directly). You can disable auto-redraw specifically for a single event by setting e.redraw = false on it:

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

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(vnode) {
+        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. +
+
+ + + + + + diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 00000000..31d9a943 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,279 @@ + + + + Introduction - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+

Mithril supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required.

+

Looking for the v1 docs? Click here.

+
+

Getting started

+

An easy 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>
+    <script src="https://unpkg.com/mithril@next/mithril.js"></script>
+    <script>
+    var root = document.body
+
+    // your code goes here!
+    </script>
+</body>
+

To make things simpler you can fork this pen which already has the latest version of mithril loaded.

+

See the Pen Mithril Scaffold by Pat Cavit (@tivac) on CodePen.

+ + +

Mithril is also loaded onto this page already, so you can start poking at the m object in the developer console right away if you'd like!

+
+

Hello world

+

Let's start as small as we 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.

+

Live Example

+

See the Pen Mithril Hello World by Pat Cavit (@tivac) on CodePen.

+ + +
+

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 need 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"),
+])
+

Live Example

+

See the Pen Simple Mithril Example by Pat Cavit (@tivac) on CodePen.

+ + +

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.

+

Live Example

+

See the Pen Mithril Component Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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're 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.

+

Live Example

+

See the Pen Mithril Routing Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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 withCredentials means to enable cookies (a requirement for the REM API to work)

+
var count = 0
+var increment = function() {
+    m.request({
+        method: "PUT",
+        url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
+        data: {count: count + 1},
+        withCredentials: 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.

+

Live Example

+

See the Pen Mithril XHR Example by Pat Cavit (@tivac) on CodePen.

+ + +
+

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. +
+
+ + + + + + diff --git a/dist/installation.html b/dist/installation.html new file mode 100644 index 00000000..8c4ae48a --- /dev/null +++ b/dist/installation.html @@ -0,0 +1,245 @@ + + + + Installation - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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="https://unpkg.com/mithril@next/mithril.js"></script>
+
+

NPM

+
$ npm install mithril@next --save
+
+

Quick start with Webpack

+
    +
  1. Initialize the directory as an npm package

    +
    $ npm init --yes
    +
  2. +
  3. install required tools

    +
    $ npm install mithril@next --save
    +$ npm install webpack webpack-cli --save-dev
    +
  4. +
  5. Add a "start" entry to the scripts section in package.json.

    +
    {
    + // ...
    + "scripts": {
    +     "start": "webpack src/index.js --output bin/app.js -d --watch"
    + }
    +}
    +
  6. +
  7. Create src/index.js file.

    +
    import m from "mithril";
    +m.render(document.body, "hello world");
    +
  8. +
  9. create index.html

    +
    <!DOCTYPE html>
    +<body>
    + <script src="bin/app.js"></script>
    +</body>
    +
  10. +
  11. run bundler

    +
    $ npm start
    +
  12. +
  13. open index.html in a browser

    +
  14. +
+

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@next --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 webpack-cli --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 --output 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 --output bin/app.js -d --watch",
+        "build": "webpack src/index.js --output 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@next --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 src/index.js --output bin/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="https://unpkg.com/mithril@next/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")
+
+

TypeScript

+

TypeScript type definitions are available from DefinitelyTyped. They can be installed with:

+
$ npm install @types/mithril --save-dev
+

For example usage, to file issues or to discuss TypeScript related topics visit: https://github.com/MithrilJS/mithril.d.ts

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/integrating-libs.html b/dist/integrating-libs.html new file mode 100644 index 00000000..6ee1e2f1 --- /dev/null +++ b/dist/integrating-libs.html @@ -0,0 +1,197 @@ + + + + 3rd Party Integration - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

3rd Party Integration

+ +

Integration with third party libraries or vanilla JavaScript code can be achieved via lifecycle methods.

+

noUiSlider Example

+
/** NoUiSlider wrapper component */
+function Slider() {
+    var slider
+
+    return {
+        oncreate: function(vnode) {
+            // Initialize 3rd party lib here
+            slider = noUiSlider.create(vnode.dom, {
+                start: 0,
+                range: {min: 0, max: 100}
+            })
+            slider.on('update', function(values) {
+                vnode.attrs.onChange(values[0])
+                m.redraw()
+            })
+        },
+        onremove: function() {
+            // Cleanup 3rd party lib on removal
+            slider.destroy()
+        },
+        view: function() {
+            return m('div')
+        }
+    }
+}
+
+/** Demo app component */
+function Demo() {
+    var showSlider = false
+    var value = 0
+
+    return {
+        view: function() {
+            return m('.app',
+                m('p',
+                    m('button',
+                        {
+                            type: 'button',
+                            onclick: function() {
+                                showSlider = !showSlider
+                            }
+                        },
+                        showSlider ? "Destroy Slider" : "Create Slider"
+                    )
+                ),
+                showSlider && m(Slider, {
+                    onChange: function(v) {
+                        value = v
+                    }
+                }),
+                m('p', value)
+            )
+        }
+    }
+}
+
+m.mount(document.body, Demo)
+

Live Demo

+

Bootstrap FullCalendar Example

+
/** FullCalendar wrapper component */
+var FullCalendar = {
+    oncreate: function (vnode) {
+        console.log('FullCalendar::oncreate')
+        $(vnode.dom).fullCalendar({
+            // put your initial options and callbacks here
+        })
+    },
+    onremove: function (vnode) {
+        // Run any destroy / cleanup methods here.
+        $(vnode.dom).fullCalendar('destroy')
+    },
+    view: function (vnode) {
+        return m('div')
+    }
+}
+
+/** Demo app component */
+function Demo() {
+    var fullCalendarEl
+
+    function next() {
+        $(fullCalendarEl).fullCalendar('next')
+    }
+
+    function prev() {
+        $(fullCalendarEl).fullCalendar('prev')
+    }
+
+    return {
+        view: function (vnode) {
+            return [
+                m('h1', 'Calendar'),
+                m(FullCalendar, {
+                    oncreate: function(vnode) {
+                        fullCalendarEl = vnode.dom
+                    }
+                }),
+                m('button', {
+                    onclick: prev
+                }, 'Mithril Button -'),
+                m('button', {
+                    onclick: next
+                }, 'Mithril Button +')
+            ]
+        }
+    }
+}
+
+m.mount(document.body, Demo)
+

Live Demo

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/jsonp.html b/dist/jsonp.html new file mode 100644 index 00000000..19bc44b9 --- /dev/null +++ b/dist/jsonp.html @@ -0,0 +1,206 @@ + + + + jsonp(options) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
optionsObjectYesThe request options to pass.
options.urlStringYesThe path name to send the request to, optionally interpolated with values from options.data.
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)
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 type method
+

promise = m.jsonp(url, options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringYesThe path name to send the request to. options.url overrides this when present.
optionsObjectNoThe request options to pass.
returnsPromiseA promise that resolves to the response data, after it has been piped through the type method
+

This second form is mostly equivalent to m.jsonp(Object.assign({url: url}, options)), just it does not depend on the ES6 global Object.assign internally.

+

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. +
+
+ + + + + + diff --git a/dist/jsx.html b/dist/jsx.html new file mode 100644 index 00000000..0c354f2d --- /dev/null +++ b/dist/jsx.html @@ -0,0 +1,252 @@ + + + + JSX - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.render(document.body, <MyComponent />)
+// equivalent to m.render(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

+
const path = require('path')
+
+module.exports = {
+    entry: './src/index.js',
+    output: {
+        path: path.resolve(__dirname, './bin'),
+        filename: 'app.js',
+    },
+    module: {
+        rules: [{
+            test: /\.js$/,
+            exclude: /node_modules/,
+            loader: 'babel-loader'
+        }]
+    }
+}
+

For those familiar with Webpack already, please note that adding the Babel options to the babel-loader section of your webpack.config.js will throw an error, so you need to include them in the separate .babelrc file.

+

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:

+ +

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. +
+
+ + + + + + diff --git a/dist/keys.html b/dist/keys.html new file mode 100644 index 00000000..e6b3c140 --- /dev/null +++ b/dist/keys.html @@ -0,0 +1,231 @@ + + + + Keys - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 make a user component, the key must be moved out of the component and put on the component itself, since it is now the immediate child of the array.

+
// AVOID
+var User = {
+    view: function(vnode) {
+        return m("div", { key: vnode.attrs.user.id }, [
+      m(Button, vnode.attrs.user.name)
+    ])
+    }
+}
+
+// PREFER
+users.map(function(u) {
+    return m(User, { key: u.id, user: u }) // 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"},
+]
+

Avoid mixing keyed and non-keyed vnodes in the same array

+

An array of vnodes must have only keyed vnodes or non-keyed vnodes, but not both. If you need to mix them, create a nested array.

+
// AVOID
+m("div", [
+    m("div", "a"),
+    m("div", {key: 1}, "b"),
+])
+
+// PREFER
+m("div", [
+    m("div", {key: 0}, "a"),
+    m("div", {key: 1}, "b"),
+])
+
+
+// PREFER
+m("div", [
+    m("div", "a"),
+    [
+        m("div", {key: 1}, "b"),
+    ]
+])
+

Avoid passing model data directly to components if the model uses key as a data property

+

The key property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose key property is expected to change over time. This can lead to components receiving the wrong data, re-initialize, or change positions unexpectedly. If your data model uses the key property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction:

+
// Data model
+var users = [
+    {id: 1, name: "John", key: 'a'},
+    {id: 2, name: "Mary", key: 'b'},
+]
+
+// Later on...
+users[0].key = 'c'
+
+// AVOID
+users.map(function(user){
+    // The component for John will be destroyed and recreated
+    return m(UserComponent, user)
+})
+
+// PREFER
+users.map(function(user){
+    // Key is specifically extracted: data model is given its own property
+    return m(UserComponent, {key: user.id, model: user})
+})
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/layout.html b/dist/layout.html new file mode 100644 index 00000000..fa06f4f1 --- /dev/null +++ b/dist/layout.html @@ -0,0 +1,42 @@ + + + + Mithril.js + + + + + + +
+
+ +

Mithril [version]

+ +
+
+
+
+ [body] +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/learning-mithril.html b/dist/learning-mithril.html new file mode 100644 index 00000000..f10872c2 --- /dev/null +++ b/dist/learning-mithril.html @@ -0,0 +1,89 @@ + + + + Learning Resources - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Learning Resources

+ +

Links to Mithril learning content:

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/lifecycle-methods.html b/dist/lifecycle-methods.html new file mode 100644 index 00000000..16047081 --- /dev/null +++ b/dist/lifecycle-methods.html @@ -0,0 +1,228 @@ + + + + Lifecycle methods - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+
function ComponentWithState() {
+    var initialData
+    return {
+        oninit: function(vnode) {
+            initialData = vnode.attrs.data
+        },
+        view: function(vnode) {
+            return [
+                // displays data from initialization time:
+                m("div", "Initial: " + initialData),
+                // displays current data:
+                m("div", "Current: " + vnode.attrs.data)
+            ]
+        }
+    }
+}
+
+m(ComponentWithState, {data: "Hello"})
+

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.

+

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.

+
function RedrawReporter() {
+    var count = 0
+    return {
+        onupdate: function() {
+            console.log("Redraws so far: ", ++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.

+
function Timer() {
+    var timeout = setTimeout(function() {
+        console.log("timed out")
+    }, 1000)
+
+    return {
+        onremove: function() {
+            clearTimeout(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. +
+
+ + + + + + diff --git a/dist/logo.svg b/dist/logo.svg new file mode 100644 index 00000000..ebac4eb3 --- /dev/null +++ b/dist/logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/mount.html b/dist/mount.html new file mode 100644 index 00000000..4086a6a6 --- /dev/null +++ b/dist/mount.html @@ -0,0 +1,143 @@ + + + + mount(root, component) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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)
+

To pass arguments when mounting a component use:

+
m.mount(element, {view: function () {return m(Component, attrs)}})
+
+

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

+

m.mount(element, Component), when called renders the component into the element and subscribe the (element, Component) pair to the redraw subsystem. That tree will be re-rendered when manual or automatic redraws are triggered.

+

On redraw, the new vDOM tree is compared (or "diffed") with the old one, 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 redraws in response to view events, m.redraw() calls or m.request() calls. Vnodes rendered via m.render() do not.

+

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. +
+
+ + + + + + diff --git a/dist/parsePathname.html b/dist/parsePathname.html new file mode 100644 index 00000000..e7c96485 --- /dev/null +++ b/dist/parsePathname.html @@ -0,0 +1,107 @@ + + + + parsePathname(string) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

parsePathname(string)

+ +
+

Description

+

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

+
var object = m.parsePathname("/path/user?a=1&b=2")
+// {path: "/path/user", params: {a: "1", b: "2"}}
+
+

Signature

+

object = m.parsePathname(string)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
stringStringYesA URL
returnsObjectA {path, params} pair where path is the normalized path and params is the parsed parameters.
+

How to read signatures

+
+

How it works

+

The m.parsePathname method creates an object from a path with a possible query string and hash string. It is useful for parsing a URL into more sensible paths, and it's what m.route uses internally to normalize paths to later match them. It uses m.parseQueryString to parse the query parameters into an object.

+
var data = m.parsePathname("/path/user?a=hello&b=world#random=hash&some=value")
+
+// data.path is "/path/user"
+// data.params is {a: "hello", b: "world", random: "hash", some: "value"}
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/parseQueryString.html b/dist/parseQueryString.html new file mode 100644 index 00000000..9c40886e --- /dev/null +++ b/dist/parseQueryString.html @@ -0,0 +1,132 @@ + + + + parseQueryString(string) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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, object)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
stringStringYesA querystring
objectObjectNoAn existing key-value map to merge values into, potentially from a previous m.parseQueryString call
returnsObjectA key-value map, object if provided
+

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. +
+
+ + + + + + diff --git a/dist/paths.html b/dist/paths.html new file mode 100644 index 00000000..d15f3f63 --- /dev/null +++ b/dist/paths.html @@ -0,0 +1,183 @@ + + + + Path Handling - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Path Handling

+ +
+

m.route, m.request, and m.jsonp each have a concept called a path. This is used to generate the URL you route to or fetch from.

+

Path types

+

There are two general types of paths: raw paths and parameterized paths.

+ +

For m.request and m.jsonp, these can be pretty much any URL, but for routes, these can only be absolute URL path names without schemes or domains.

+

Path parameters

+

Path parameters are themselves pretty simple. They come in two forms:

+ +

You're probably wondering what that params object is supposed to be. It's pretty simple: it's the params in either m.route.set(path, params), m.request({url, params}), or m.jsonp({url, params}).

+

When receiving routes via m.route(root, defaultRoute, routes), you can use these parameters to extract values from routes. They work basically the same way as generating the paths, just in the opposite direction.

+
// Edit a single item
+m.route(document.body, "/edit/1", {
+    "/edit/:id": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing user " + m.route.param("id"))
+            ]
+        }
+    },
+})
+
+// Edit an item identified by path
+m.route(document.body, "/edit/pictures/image.jpg", {
+    "/edit/:file...": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing file " + m.route.param("file"))
+            ]
+        }
+    },
+})
+

In the first example, assuming you're navigating to the default route in each, m.route.param("id") would be read as "1" and m.route.param("file") would be read as pictures/image.jpg.

+

Path parameters may be delimited by either a /, -, or .. This lets you have dynamic path segments, and they're considerably more flexible than just a path name. For example, you could match against routes like "/edit/:name.:ext" for editing based on file extension or "/:lang-:region/view" for a localized route.

+

Path parameters are greedy: given a declared route "/edit/:name.:ext", if you navigate to /edit/file.test.png, the parameters extracted will be {name: "file.test", ext: "png"}, not {name: "file", ext: "test.png"}. Similarly, given "/route/:path.../view/:child...", if you go to /route/foo/view/bar/view/baz, the parameters extracted will be {path: "foo/view/bar", child: "baz"}.

+

Parameter normalization

+

Path parameters that are interpolated into path names are omitted from the query string, for convenience and to keep the path name reasonably readable. For example, this sends a server request of GET /api/user/1/connections?sort=name-asc, omitting the duplicate id=1 in the URL string.

+
m.request({
+    url: "https://example.com/api/user/:userID/connections",
+    params: {
+        userID: 1,
+        sort: "name-asc"
+    }
+})
+

You can also specify parameters explicitly in the query string itself, such as in this, which is equivalent to the above:

+
m.request({
+    url: "https://example.com/api/user/:userID/connections?sort=name-asc",
+    params: {
+        userID: 1
+    }
+})
+

And of course, you can mix and match. This fires a request to GET /api/user/1/connections?sort=name-asc&first=10.

+
m.request({
+    url: "https://example.com/api/user/:userID/connections?sort=name-asc",
+    params: {
+        userID: 1,
+        first: 10
+    }
+})
+

This even extends to route matching: you can match against a route with explicit query strings. It retains the matched parameter for convenience, so you can still access them via vnode parameters or via m.route.param. Note that although this is possible, it's not generally recommended, since you should prefer paths for pages. It could sometimes useful if you need to generate a somewhat different view just for a particular file type, but it still logically is a query-like parameter, not a whole separate page.

+
// Note: this is generally *not* recommended - you should prefer paths for route
+// declarations, not query strings.
+m.route(document.body, "/edit/1", {
+    "/edit?type=image": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing photo")
+            ]
+        }
+    },
+    "/edit": {
+        view: function() {
+            return [
+                m(Menu),
+                m("h1", "Editing " + m.route.param("type"))
+            ]
+        }
+    }
+})
+

Note that query parameters are implicit - you don't need to name them to accept them. You can match based on an existing value, like in "/edit?type=image", but you don't need to use "/edit?type=:type" to accept the value. In fact, Mithril would treat that as you trying to literally match against m.route.param("type") === ":type". Or in summary, use m.route.param("key") to extract parameters - it simplifies things.

+

Path normalization

+

Parsed paths are always returned with all the duplicate parameters and extra slashes dropped, and they always start with a slash. These little differences often get in the way, and it makes routing and path handling a lot more complicated than it should be. Mithril internally normalizes paths for routing, but it does not expose the current, normalized route directly. (You could compute it via m.parsePathname(m.route.get()).path.)

+

When parameters are deduplicated during matching, parameters in the query string are preferred over parameters in the path name, and parameters towards the end of the URL are preferred over parameters closer to the start of the URL.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/promise.html b/dist/promise.html new file mode 100644 index 00000000..66a0ada8 --- /dev/null +++ b/dist/promise.html @@ -0,0 +1,439 @@ + + + + Promise(executor) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Promise(executor)

+ +
+

Description

+

A ES6 Promise polyfill.

+

A Promise is a mechanism for working with asynchronous computations.

+

Mithril provides a polyfill when the environment does not support Promises. The polyfill can also be referenced specifically via m.PromisePolyfill.

+
+

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 asynchronous 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. +
+
+ + + + + + diff --git a/dist/redraw.html b/dist/redraw.html new file mode 100644 index 00000000..4967a992 --- /dev/null +++ b/dist/redraw.html @@ -0,0 +1,133 @@ + + + + redraw() - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. The autoredraw system, which is built on top of m.redraw() will take care of it.

+

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

+
+

Signature

+

m.redraw()

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

Static members

+
m.redraw.sync
+

m.redraw.sync()

+ + + + + + + + + + + + + + + +
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.

+

m.redraw() always triggers an asynchronous redraws, whereas m.redraw.sync() triggers a synchronous one. m.redraw() is tied to window.requestAnimationFrame(). It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.

+

m.redraw.sync() is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat:

+ +

m.redraw() doesn't have any of those issues, you can call it from wherever you like.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/releasing.html b/dist/releasing.html new file mode 100644 index 00000000..e2e4876d --- /dev/null +++ b/dist/releasing.html @@ -0,0 +1,201 @@ + + + + Mithril Release Processes - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Mithril Release Processes

+ +

Note These steps all assume that MithrilJS/mithril.js is a git remote named mithriljs, adjust accordingly if that doesn't match your setup.

+ +

Releasing a new Mithril version

+

Prepare the release

+
    +
  1. Ensure your local branch is up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Determine patch level of the change
  2. +
  3. Update information in docs/change-log.md to match reality of the new version being prepared for release
  4. +
  5. Replace all existing references to mithril@next to mithril if moving from a release candidate to stable.
  6. +
  7. Commit changes to next
  8. +
+
$ git add .
+$ git commit -m "Preparing for release"
+
+# Push to your branch
+$ git push
+
+# Push to MithrilJS/mithril.js
+$ git push mithriljs next

Merge from next to master

+
    +
  1. Switch to master and make sure it's up to date
  2. +
+
$ git checkout master
+$ git pull --rebase mithriljs master
+
    +
  1. merge next on top of it
  2. +
+
$ git merge next
+
    +
  1. Clean & update npm dependencies and ensure the tests are passing.
  2. +
+
$ npm prune
+$ npm i
+$ npm test
+

Publish the release

+
    +
  1. npm run release <major|minor|patch|semver>, see the docs for npm version
  2. +
  3. The changes will be automatically pushed to your fork
  4. +
  5. Push the changes to MithrilJS/mithril.js
  6. +
+
$ git push mithriljs master
+
    +
  1. Travis will push the new release to npm & create a GitHub release
  2. +
+

Merge master back into next

+

This helps to ensure that the version field of package.json doesn't get out of date.

+
    +
  1. Switch to next and make sure it's up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Merge master back onto next
  2. +
+
$ git merge master
+
    +
  1. Push the changes to your fork & MithrilJS/mithril.js
  2. +
+
$ git push
+$ git push mithriljs next
+

Update the GitHub release

+
    +
  1. The GitHub Release will require a manual description & title to be added. I suggest coming up with a fun title & then copying the docs/change-log.md entry for the build.
  2. +
+

Updating mithril.js.org

+

Fixes to documentation can land whenever, updates to the site are published via Travis.

+
# These steps assume that MithrilJS/mithril.js is a git remote named "mithriljs"
+
+# Ensure your next branch is up to date
+$ git checkout next
+$ git pull mithriljs next
+
+# Splat the docs folder from next onto master
+$ git checkout master
+$ git checkout next -- ./docs
+
+# Manually ensure that no new feature docs were added
+
+$ git push mithriljs
+

After the Travis build completes the updated docs should appear on https://mithril.js.org in a few minutes.

+

Note: When updating the stable version with a release candidate out, make sure to update the index + navigation to point to the new stable version!!!

+

Releasing a new ospec version

+
    +
  1. Ensure your local branch is up to date
  2. +
+
$ git checkout next
+$ git pull --rebase mithriljs next
+
    +
  1. Determine patch level of the change
  2. +
  3. Update version field in ospec/package.json to match new version being prepared for release
  4. +
  5. Commit changes to next
  6. +
+
$ git add .
+$ git commit -m "chore(ospec): ospec@<version>"
+
+# Push to your branch
+$ git push
+
+# Push to MithrilJS/mithril.js
+$ git push mithriljs next

Merge from next to master

+
    +
  1. Switch to master and make sure it's up to date
  2. +
+
$ git checkout master
+$ git pull --rebase mithriljs master
+
    +
  1. merge next on top of it
  2. +
+
$ git checkout next -- ./ospec
+$ git add .
+$ git commit -m "chore(ospec): ospec@<version>"
+
    +
  1. Ensure the tests are passing!
  2. +
+

Publish the release

+
    +
  1. Push the changes to MithrilJS/mithril.js
  2. +
+
$ git push mithriljs master
+
    +
  1. Publish the changes to npm from the /ospec folder. That bit is important to ensure you don't accidentally ship a new Mithril release!
  2. +
+
$ cd ./ospec
+$ npm publish
+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/render.html b/dist/render.html new file mode 100644 index 00000000..fd20cd09 --- /dev/null +++ b/dist/render.html @@ -0,0 +1,133 @@ + + + + render(element, vnodes) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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-sufficient. 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. +
+
+ + + + + + diff --git a/dist/request.html b/dist/request.html new file mode 100644 index 00000000..03c55bb8 --- /dev/null +++ b/dist/request.html @@ -0,0 +1,581 @@ + + + + request(options) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
optionsObjectYesThe request options to pass.
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 path name to send the request to, optionally interpolated with values from options.data.
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.timeoutNumberNoThe amount of milliseconds a request can take before automatically being terminated. Defaults to undefined.
options.responseTypeStringNoThe expected type of the response. Defaults to "" if extract is defined, "json" if missing. If responseType: "json", it internally performs JSON.parse(responseText).
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(any)NoA deserialization method to be applied to the xhr.response or normalized xhr.responseText. Defaults to the identity function. If extract is defined, deserialize will be skipped.
options.extractany = Function(xhr, options)NoA hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns options.deserialize(parsedResponse), throwing an exception when the server response status code indicates an error or when the response is syntactically invalid. If a custom extract callback is provided, the xhr parameter is the XMLHttpRequest instance used for the request, and options is the object that was passed to the m.request call. Additionally, deserialize will be skipped and the value returned from the extract callback will be left as-is when the promise resolves.
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
+

promise = m.request(url, options)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
urlStringYesThe path name to send the request to. options.url overrides this when present.
optionsObjectNoThe request options to pass.
returnsPromiseA promise that resolves to the response data, after it has been piped through the extract, deserialize and type methods
+

This second form is mostly equivalent to m.request(Object.assign({url: url}, options)), just it does not depend on the ES6 global Object.assign internally.

+

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 returns a promise and triggers 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).

+

If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection.

+
+

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.

+
+

Error handling

+

When a non-file: request returns with any status other than 2xx or 304, it rejects with an error. This error is a normal Error instance, but with a few special properties.

+ +

This is useful in many cases where errors are themselves things you can account for. If you want to detect if a session expired - you can do if (error.code === 401) return promptForAuth().then(retry). If you hit an API's throttling mechanism and it returned an error with a "timeout": 1000, you could do a setTimeout(retry, error.response.timeout).

+
+

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, files[i])
+    }
+
+    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.upload.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 xhr.responseText as JSON and returns the parsed object. It may be useful to inspect a server response in more detail and process it manually. 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:

+ +

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:

+ +

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. +
+
+ + + + + + diff --git a/dist/route.html b/dist/route.html new file mode 100644 index 00000000..906a248d --- /dev/null +++ b/dist/route.html @@ -0,0 +1,779 @@ + + + + route(root, defaultRoute, routes) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. Triggers an asynchronous redraw off all mount points.

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
pathStringYesThe path name to route to, without a prefix. The path may include parameters, interpolated with values from data.
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
+

Remember that when using .set with params you also need to define the route:

+
var Article = {
+    view: function(vnode) {
+        return "This is article " + vnode.attrs.articleid
+    }
+}
+
+m.route(document.body, {
+    '/article/:articleid': Article
+})
+m.route.set('/article/:articleid', {articleid: 1})
+
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
+ +

This function can be used as the oncreate (and onupdate) hook in a m("a") vnode:

+
m("a[href=/]", {oncreate: m.route.link})
+

Using m.route.link as a oncreate hook causes the link to behave as a router link (i.e. it navigates to the route specified in href, instead of navigating away from the current page to the URL specified in href.

+

If the href attribute is not static, the onupdate hook must also be set:

+
m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link})
+

m.route.link can also set the options passed to m.route.set when the link is clicked by calling the function in the lifecycle methods:

+
m("a[href=/]", {oncreate: m.route.link({replace: true})})
+

m.route.link(args)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
argsVnode|ObjectYesThis method is meant to be used as or in conjunction with an <a> vnode's oncreate and onupdate hooks
returnsfunctionReturns the onclick handler function for the component
+
m.route.param
+

Retrieves a route parameter from the last fully resolved route. A route parameter is a key-value pair. Route parameters may come from a few different places:

+ +

value = m.route.param(key)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
keyStringNoA route parameter name (e.g. id in route /users/:id, or page in path /users/1?page=3, or a key in history.state)
returnsString|ObjectReturns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys
+

Note that in the onmatch function of a RouteResolver, the new route hasn't yet been fully resolved, and m.route.param() will return the parameters of the previous route, if any. onmatch receives the parameters of the new route as an argument.

+

RouteResolver

+

A RouteResolver is a non-component object that contains an onmatch method and/or a render method. Both methods are optional, but at least one must be present.

+

If an object can be detected as a component (by the presence of a view method or by being a function/class), it will be treated as such even if it has onmatch or render methods. Since a RouteResolver is not a component, 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, route)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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.
routeStringThe router path requested by the last routing action, excluding interpolated routing parameter values
returnsComponent|Promise|undefinedReturns 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
returnsArray<Vnode>|VnodeThe vnodes to be rendered
+
+

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 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 hash strategy is guaranteed to work in browsers that don't support history.pushState, because it can fall back to using onhashchange. Use this strategy if you want to keep the hashes purely local.

+

The querystring strategy allows server-side detection, but it doesn't appear as a normal path. Use this strategy if you want to support and potentially detect anchored links server-side and you are not able to make the changes necessary to support the pathname strategy (like if you're using Apache and can't modify your .htaccess).

+

The pathname strategy produces the cleanest looking URLs, but 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.

+

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.

+

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", {
+    "/edit/:file...": Edit,
+})
+

Handling 404s

+

For isomorphic / universal JavaScript app, an url param and a variadic route combined is very useful to display custom 404 error page.

+

In a case of 404 Not Found error, the server send back the custom page to client. When Mithril is loaded, it will redirect client to the default route because it can't know that route.

+
m.route(document.body, "/", {
+  "/": homeComponent,
+  // [...]
+  "/:404...": errorPageComponent
+});
+

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: function (e) { state.term = e.target.value },
+                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, route) {
+            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 needed.

+

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 has oninit and oncreate lifecycle methods, they would only fire 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 RouteResolver's onmatch hook can be used to run logic before the top level component in a route is initialized. 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: function (e) { Auth.setUsername(e.target.value) },
+                value: Auth.username
+            }),
+            m("input[type=password]", {
+                oninput: function (e) { Auth.setPassword(e.target.value) },
+                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. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that loadUsers() returns a Promise, but any Promise returned by oninit is currently ignored. The second render pass comes from the background option for m.request.

+
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. +
+
+ + + + + + diff --git a/dist/signatures.html b/dist/signatures.html new file mode 100644 index 00000000..2b0e7f0d --- /dev/null +++ b/dist/signatures.html @@ -0,0 +1,126 @@ + + + + How to read signatures - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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. +
+
+ + + + + + diff --git a/dist/simple-application.html b/dist/simple-application.html new file mode 100644 index 00000000..676274fa --- /dev/null +++ b/dist/simple-application.html @@ -0,0 +1,535 @@ + + + + Simple application - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Simple application

+ +

Let's develop a simple application that covers some of the major aspects of Single Page Applications

+

An interactive running example can be seen here flems: Simple Application

+

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 https://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: "https://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:

+
// src/views/UserList.js
+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 deviates 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=button]", "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: "https://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: "https://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: "https://rem-rest-api.herokuapp.com/api/users/" + 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=button]", "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", {
+                onsubmit: function(e) {
+                    e.preventDefault()
+                    User.save()
+                }
+            }, [
+            m("label.label", "First name"),
+            m("input.input[type=text][placeholder=First name]", {
+                oninput: function (e) {User.current.firstName = e.target.value},
+                value: User.current.firstName
+            }),
+            m("label.label", "Last name"),
+            m("input.input[placeholder=Last name]", {
+                oninput: function (e) {User.current.lastName = e.target.value},
+                value: User.current.lastName
+            }),
+            m("button.button[type=submit]", "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: "https://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: "https://rem-rest-api.herokuapp.com/api/users/" + id,
+            withCredentials: true,
+        })
+        .then(function(result) {
+            User.current = result
+        })
+    },
+
+    save: function() {
+        return m.request({
+            method: "PUT",
+            url: "https://rem-rest-api.herokuapp.com/api/users/" + User.current.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:

+
// 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.id} />.

+

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. +
+
+ + + + + + diff --git a/dist/stream.html b/dist/stream.html new file mode 100644 index 00000000..05e5dcc6 --- /dev/null +++ b/dist/stream.html @@ -0,0 +1,704 @@ + + + + stream() - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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")
+

You can also download the module directly if your environment does not support a bundling toolchain:

+
<script src="https://unpkg.com/mithril@next/stream/stream.js"></script>
+

When loaded directly with a <script> tag (rather than required), the stream library will be exposed as window.m.stream. If window.m is already defined (e.g. because you also use the main Mithril script), it will attach itself to the existing object. Otherwise it creates a new window.m. If you want to use streams in conjunction with Mithril as raw script tags, you should include Mithril in your page before mithril/stream, because mithril will otherwise overwrite the window.m object defined by mithril/stream. This is not a concern when the libraries are consumed as CommonJS modules (using require(...)).

+
+

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.scan
+

Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value.

+

Note that you can prevent dependent streams from being updated by returning the special value stream.SKIP inside the accumulator function.

+

stream = Stream.scan(fn, accumulator, stream)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
fn(accumulator, value) -> result | SKIPYesA function that takes an accumulator and value parameter and returns a new accumulator value of the same type
accumulatoranyYesThe starting value for the accumulator
streamStreamYesStream containing the values
returnsStreamReturns a new stream containing the result
+

How to read signatures

+
+
Stream.scanMerge
+

Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream.

+

stream = Stream.scanMerge(pairs, accumulator)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
pairsArray<[Stream, (accumulator, value) -> value]>YesAn array of tuples of stream and scan functions
accumulatoranyYesThe starting value for the accumulator
returnsStreamReturns a new stream containing the result
+

How to read signatures

+
+
Stream.lift
+

Creates a computed stream that reactively updates if any of its upstreams are updated. See combining streams. Unlike combine, the input streams are a variable number of arguments (instead of an array) and the callback receives the stream values instead of streams. There is no changed parameter. This is generally a more user-friendly function for applications than combine.

+

stream = Stream.lift(lifter, stream1, stream2, ...)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
lifter(any...) -> anyYesSee lifter argument
streams...list of StreamsYesStreams to be lifted
returnsStreamReturns a stream
+

How to read signatures

+
+
lifter
+

Specifies how the value of a computed stream is generated. See combining streams

+

any = lifter(streams...)

+ + + + + + + + + + + + + + + + + + + + + +
ArgumentTypeRequiredDescription
streams...splat of StreamsNoSplat of zero or more streams that correspond to the streams passed to stream.lift
returnsanyReturns a computed value
+

How to read signatures

+
+
Stream.SKIP
+

A special value that can be returned to stream callbacks to skip 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/map"](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 event callbacks and similar.

+
// a stream
+var user = stream("")
+
+// a bi-directional binding to the stream
+m("input", {
+    oninput: function (e) { user(e.target.value) },
+    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.SKIP

+
var skipped = stream(1).map(function(value) {
+    return stream.SKIP
+})
+
+skipped.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"
+

Or you can use the helper function stream.lift()

+
var a = stream("hello")
+var b = stream("world")
+
+var greeting = stream.lift(function(_a, _b) {
+    return _a + " " + _b
+}, a, b)
+
+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 brings the performance benefits of not recomputing downstreams unnecessarily.

+

You can prevent dependent streams from being updated by returning the special value stream.SKIP

+
var skipped = stream.combine(function(stream) {
+    return stream.SKIP
+}, [stream(1)])
+
+skipped.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 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. +
+
+ + + + + + diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 00000000..60b3a9a3 --- /dev/null +++ b/dist/style.css @@ -0,0 +1,97 @@ +body {background:white;-webkit-text-size-adjust: 100%;} +body,table,h5 {font-weight:normal;font-size:16px;font-family:'Open Sans',sans-serif;} +header,main {margin:auto;max-width:1000px;} +header section {position:absolute;width:250px;} +nav a {border-left:1px solid #ddd;padding:0 10px;} +nav a:first-child {border:0;padding-left:0;} +main {margin-bottom:100px;} +main section {margin-left:270px;} +p {margin:0 0 15px;} +pre,code {background:#eee;font-family:monospace;font-size:14px;} +pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;margin:20px 0;} +code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:5px 3px;white-space:pre;} +pre code {border:0;margin:0;padding:0;} +table {border-collapse:collapse;margin:0 0 30px;width:100%;} +tbody tr:nth-child(odd) {background:#fafafa;} +thead tr,tbody tr:nth-child(even) {background:#f3f3f3;} +tr {border-bottom:1px solid #eee;} +th {text-align:left;} +th,td {padding:3px 10px;vertical-align:top;} +a {color:#1e5799;text-decoration:none;} +a:hover {text-decoration:underline;} +hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;} + +/* Headings */ +h1,h2,h3,h4,h5 {position:relative} +h1 {font-size:24px;margin:0 0 15px;} +h2 {font-size:22px;margin:45px 0 15px;} +h3 {font-size:20px;margin:45px 0 15px;} +h4 {font-size:18px;margin:30px 0 15px;} +h5 {font-weight:bold;margin:15px 0 15px;} +h1 img {transform:rotate(180deg);vertical-align:middle;width:20px;} +h1 small {font-size:16px;} +h2 a,h3 a,h4 a,h5 a, +h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover, +h2 a:active,h3 a:active,h4 a:active,h5 a:active, +h2 a:visited,h3 a:visited,h4 a:visited,h5 a:visited {color:#000;text-decoration:none;} +h2::before,h3::before,h4::before,h5::before {content:"#";position:absolute;left:-20px;visibility:hidden;} +h2:hover::before,h3:hover::before,h4:hover::before,h5:hover::before {visibility:visible;} +#signature + p code {padding:3px 10px;} +h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;z-index:1;} +h1 + ul + hr {display:none;} +h1 + ul li {list-style:none;margin:0;padding:0;} +h1 + ul li:last-child {border-bottom:0;} +h1 + ul ul {margin:0 0 2px;padding:0 0 0 15px;} +h1 + ul ul li {border:0;} +h1 + ul strong + ul {border-left:3px solid #1e5799;} + +.hamburger {display:none;} + +@keyframes grow { + from {transform:scaleX(0)} + to {transform:scaleX(100%)} +} + +@media (max-width: 767px) { + .hamburger {display:block;font-size:30px;padding:0 10px;position:fixed;right:0;top:0;z-index:2;} + .hamburger:hover {text-decoration:none;} + main section {margin:0;} + header section {margin:0 0 20px;position:static;width:auto;} + h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;z-index:1} + h1 + ul + hr {display:block;} + .navigating h1 + ul {display:block;} + .navigating {overflow:hidden;} +} +@media (max-width: 1024px) { + table,table tbody,table tr,table th,table td {display:block;} + table thead {display:none;} + table td:before {display:inline-block;font-style:italic;font-weight:bold;padding:0 10px 0 0;width:100px;} + table tr:not(:last-child) td:nth-child(1):before {content:"Argument:";} + table tr:last-child td:nth-child(3) {display:none;} + table td:nth-child(2):before {content:"Type:";} + table td:nth-child(3):before {content:"Required:";} + table td:nth-child(4):before {content:"Description:";} + #structure ~ table td:nth-child(1):before {content:"Property:";} + #structure ~ table td:nth-child(2):before {content:"Type:";} + #structure ~ table td:nth-child(3):before {content:"Description:";} + #vnode-types ~ table td:nth-child(1):before {content:"Vnode type:";} + #vnode-types ~ table td:nth-child(2):before {content:"Example:";} + #vnode-types ~ table td:nth-child(3):before {content:"Description:";} + #lifecycle-methods ~ table td:nth-child(1):before {content:"Hook:";} + #lifecycle-methods ~ table td:nth-child(2):before {content:"Description:";} + #react ~ table td:nth-child(1):before {content:"React:";} + #angular ~ table td:nth-child(1):before {content:"Angular:";} + #vue ~ table td:nth-child(1):before {content:"Vue:";} + #comparisons ~ table td:nth-child(2):before {content:"Mithril:";} +} +@media print { + nav,h1 + ul {display:none;} + main section {margin:0;} +} + +/* prism theming */ +.token.comment,.token.prolog,.token.doctype,.token.cdata {color:#888;} +.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol {color:#905;} +.token.selector,.token.attr-name,.token.string,.token.builtin {color:#690;} +.token.atrule,.token.attr-value,.token.punctuation,.token.keyword {color:#1e5799;} +.token.regex,.token.important {color:#e90;} diff --git a/dist/support.html b/dist/support.html new file mode 100644 index 00000000..7adcc4fb --- /dev/null +++ b/dist/support.html @@ -0,0 +1,86 @@ + + + + Getting Help - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

Getting Help

+ +

Mithril has an active & welcoming community on Gitter, or feel free to ask questions on Stack Overflow using the mithril.js tag.

+ +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/testing.html b/dist/testing.html new file mode 100644 index 00000000..13bd9fb2 --- /dev/null +++ b/dist/testing.html @@ -0,0 +1,145 @@ + + + + Testing - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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

+

Running mithril in a non-browser environment

+

Mithril has a few dependencies on globals that exist in all its supported browser environments but are missing in all non-browser environments. To work around this you can use the browser mocks that ship with the mithril npm package.

+

The simplest way to do this is ensure the following snippet of code runs before you include mithril itself in your project.

+
// Polyfill DOM env for mithril
+global.window = require("mithril/test-utils/browserMock.js")();
+global.document = window.document;
+

Once that snippet has been run you can require("mithril") and it should be quite happy.

+
+

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 inadvertently 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", 
+            m("p", "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("p")
+        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. +
+
+ + + + + + diff --git a/dist/trust.html b/dist/trust.html new file mode 100644 index 00000000..ccf55e37 --- /dev/null +++ b/dist/trust.html @@ -0,0 +1,213 @@ + + + + trust(html) - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

trust(html)

+ +
+

Description

+

Turns an HTML or SVG string into unescaped HTML or SVG. 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 or SVG 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. +
+
+ + + + + + diff --git a/dist/version.html b/dist/version.html new file mode 100644 index 00000000..3138573b --- /dev/null +++ b/dist/version.html @@ -0,0 +1,101 @@ + + + + version - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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.

+ + +
+ License: MIT. © Leo Horie. +
+
+ + + + + + diff --git a/dist/vnodes.html b/dist/vnodes.html new file mode 100644 index 00000000..08a2aaa1 --- /dev/null +++ b/dist/vnodes.html @@ -0,0 +1,237 @@ + + + + Virtual DOM nodes - Mithril.js + + + + + + +
+
+ +

Mithril 2.0.0-rc.5

+ +
+
+
+
+

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 as 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 a declarative immediate mode API, a style of rendering that makes it drastically easier to manage UI complexity.

+

To illustrate why immediate mode is so important, consider the DOM API and HTML. The DOM API is an imperative retained mode API and requires 1. writing out exact instructions to assemble a DOM tree procedurally, and 2. writing out other instructions to update that tree. 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 of introducing bugs and more chances to make code harder to understand.

+

In contrast, HTML is closer to an immediate 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 are created via the m() hyperscript utility:

+
m("div", {id: "test"}, "hello")
+

Hyperscript 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 their only child.
domElement?Points to the element that corresponds to the vnode. This property is undefined in the oninit lifecycle method. In fragments 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).
stateObject?An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the state inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure.
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 or modify it.
instanceObject?For components, a storage location for the value returned by the view. This property is only used internally by Mithril, do not use or modify it.
skipBooleanThis property is only used internally by Mithril when diffing keyed lists, do not use or modify 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. +
+
+ + + + + +