From 4e9505335cac0f79f19a352171b1a51d048f48f9 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Mon, 5 Dec 2016 11:41:20 -0800 Subject: [PATCH 01/37] Fix TOC link, remove m.request.run ref (#1456) --- docs/route.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/route.md b/docs/route.md index b16d1336..1abaea30 100644 --- a/docs/route.md +++ b/docs/route.md @@ -258,7 +258,7 @@ m.route(document.body, "/edit/pictures/image.jpg", { --- -### Changing route prefix +### Changing router prefix The router prefix is a fragment of the URL that dictates the underlying [strategy](routing-strategies.md) used by the router. @@ -402,14 +402,12 @@ module.export = { ```javascript // index.js function load(file, done) { - m.request({ - method: "GET", - url: file, + m.request(file, { extract: function(xhr) { return new Function("var module = {};" + xhr.responseText + ";return module.exports;") } }) - .run(done) + .then(done) } m.route(document.body, "/", { From 05e888958dd1ecd3f5d1c117704c5c69997eed3e Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Mon, 5 Dec 2016 16:22:33 -0800 Subject: [PATCH 02/37] Naively make m.route.set return a Promise --- router/router.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/router/router.js b/router/router.js index 6a3ac597..7d12831b 100644 --- a/router/router.js +++ b/router/router.js @@ -1,5 +1,6 @@ "use strict" +var Promise = require("../promise/promise") var buildQueryString = require("../querystring/build") var parseQueryString = require("../querystring/parse") @@ -19,12 +20,13 @@ module.exports = function($window) { var asyncId function debounceAsync(f) { return function() { - if (asyncId != null) return - asyncId = callAsync(function() { - asyncId = null - f() - }) - + return new Promise(function(resolve, reject) { + if (asyncId != null) return reject() + asyncId = callAsync(function() { + asyncId = null + resolve(f()) + }) + }); } } @@ -73,9 +75,12 @@ module.exports = function($window) { if (supportsPushState) { if (options && options.replace) $window.history.replaceState(null, null, prefix + path) else $window.history.pushState(null, null, prefix + path) - $window.onpopstate(true) + return $window.onpopstate() + } + else { + $window.location.href = prefix + path + return Promise.resolve(prefix + path) } - else $window.location.href = prefix + path } function defineRoutes(routes, resolve, reject) { @@ -116,7 +121,7 @@ module.exports = function($window) { e.redraw = false var href = this.getAttribute("href") if (href.indexOf(prefix) === 0) href = href.slice(prefix.length) - setPath(href, undefined, undefined) + return setPath(href, undefined, undefined) } } From 22e9c3bb6f479433cbda7be302490d97f11a1cb7 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Mon, 5 Dec 2016 16:23:12 -0800 Subject: [PATCH 03/37] Make api/router.js promise-aware --- api/router.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/api/router.js b/api/router.js index 95ef6afd..f4fd93f0 100644 --- a/api/router.js +++ b/api/router.js @@ -5,13 +5,13 @@ var coreRouter = require("../router/router") module.exports = function($window, redrawService) { var routeService = coreRouter($window) - + var identity = function(v) {return v} - var resolver, component, attrs, currentPath, resolve + var resolver, component, attrs, currentPath, waiting var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - resolver = routeResolver, component = comp, attrs = params, currentPath = path, resolve = null + resolver = routeResolver, component = comp, attrs = params, currentPath = path, waiting = null resolver.render = routeResolver.render || identity render() } @@ -22,14 +22,10 @@ module.exports = function($window, redrawService) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (resolve != null) update(payload, component, params, path) + if (waiting != null) update(payload, component, params, path) else { - resolve = function(resolved) { - update(payload, resolved, params, path) - } - payload.onmatch(function(resolved) { - if (resolve != null) resolve(resolved) - }, params, path) + waiting = Promise.resolve(payload.onmatch(params, path)) + .then(function() {update(payload, component, params, path)}) } } else update(payload, "div", params, path) From adabc37fd70f8d9291d23422c5ac80e476601f03 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Mon, 5 Dec 2016 17:09:02 -0800 Subject: [PATCH 04/37] Handle resolving with a component --- api/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/router.js b/api/router.js index f4fd93f0..a1f29203 100644 --- a/api/router.js +++ b/api/router.js @@ -25,7 +25,7 @@ module.exports = function($window, redrawService) { if (waiting != null) update(payload, component, params, path) else { waiting = Promise.resolve(payload.onmatch(params, path)) - .then(function() {update(payload, component, params, path)}) + .then(function(comp) {update(payload, comp != null ? comp : component, params, path)}) } } else update(payload, "div", params, path) From bc14b0568d50beeb9cf90d01a9908ea45a47b2cb Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Tue, 6 Dec 2016 01:20:17 +0000 Subject: [PATCH 05/37] Migration docs: elaborate on changes in redraw logic (#1440) * Migration docs: elaborate on changes in redraw logic * Removed bad m.request().then(m.redraw) docs --- docs/change-log.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 2698ad9d..ea2ee425 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -13,7 +13,9 @@ If you are migrating, consider using the [mithril-codemods](https://www.npmjs.co - [`m.prop` removed](#mprop-removed) - [`m.component` removed](#mcomponent-removed) - [`config` function](#config-function) -- [Cancelling redraw from event handlers](#cancelling-redraw-from-event-handlers) +- [Changes in redraw behaviour](#changes-in-redraw-behaviour) + - [No more redraw locks](#no-more-redraw-locks) + - [Cancelling redraw from event handlers](#cancelling-redraw-from-event-handlers) - [Component `controller` function](#component-controller-function) - [Component arguments](#component-arguments) - [`view()` parameters](#view-parameters) @@ -118,7 +120,15 @@ If available the DOM-Element of the vnode can be accessed at `vnode.dom`. --- -## Cancelling redraw from event handlers +## 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`. From 3134202d2461dd0d86c223ae8eca1f4bc5f4438b Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 6 Dec 2016 00:09:09 -0500 Subject: [PATCH 06/37] fix tests --- README.md | 2 +- api/router.js | 16 +++-- api/tests/test-router.js | 152 +++++++++++++++++++++++---------------- docs/route.md | 44 ++++++------ mithril.js | 17 +++-- mithril.min.js | 8 +-- router/router.js | 24 +++---- router/tests/index.html | 1 + 8 files changed, 145 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 5adbc680..ef729c3b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.42 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.44 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/api/router.js b/api/router.js index a1f29203..c5911b90 100644 --- a/api/router.js +++ b/api/router.js @@ -7,25 +7,29 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var resolver, component, attrs, currentPath, waiting + var resolver, component, attrs, currentPath, resolve var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - resolver = routeResolver, component = comp, attrs = params, currentPath = path, waiting = null + resolver = routeResolver, component = comp, attrs = params, currentPath = path, resolve = null resolver.render = routeResolver.render || identity render() } var render = function() { - if (resolver != null) redrawService.render(root, resolver.render(Vnode(component, attrs.key, attrs))) + if (resolver != null) redrawService.render(root, resolver.render(Vnode(component || "div", attrs.key, attrs))) } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (waiting != null) update(payload, component, params, path) + if (resolve != null) update(payload, component, params, path) else { - waiting = Promise.resolve(payload.onmatch(params, path)) - .then(function(comp) {update(payload, comp != null ? comp : component, params, path)}) + resolve = function(resolved) { + update(payload, resolved, params, path) + } + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + if (resolve != null) resolve(resolved) + }) } } else update(payload, "div", params, path) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 09c46eee..5ea27511 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -8,6 +8,7 @@ var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") +var Promise = require("../../promise/promise") o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { @@ -229,7 +230,7 @@ o.spec("route", function() { o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") }) - o("accepts RouteResolver", function() { + o("accepts RouteResolver", function(done) { var matchCount = 0 var renderCount = 0 var Component = { @@ -239,13 +240,13 @@ o.spec("route", function() { } var resolver = { - onmatch: function(resolve, args, requestedPath) { + onmatch: function(args, requestedPath) { matchCount++ o(args.id).equals("abc") o(requestedPath).equals("/abc") o(this).equals(resolver) - resolve(Component) + return Component }, render: function(vnode) { renderCount++ @@ -262,12 +263,15 @@ o.spec("route", function() { "/:id" : resolver }) - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") + setTimeout(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) + o(root.firstChild.nodeName).equals("DIV") + done() + }, 20) }) - o("accepts RouteResolver without `render` method as payload", function() { + o("accepts RouteResolver without `render` method as payload", function(done) { var matchCount = 0 var Component = { view: function() { @@ -278,20 +282,22 @@ o.spec("route", function() { $window.location.href = prefix + "/abc" route(root, "/abc", { "/:id" : { - onmatch: function(resolve, args, requestedPath) { + onmatch: function(args, requestedPath) { matchCount++ o(args.id).equals("abc") o(requestedPath).equals("/abc") - resolve(Component) + return Component }, }, }) - o(matchCount).equals(1) - - o(root.firstChild.nodeName).equals("DIV") + setTimeout(function() { + o(matchCount).equals(1) + o(root.firstChild.nodeName).equals("DIV") + done() + }, 20) }) o("changing `vnode.key` in `render` resets the component", function(done, timeout){ @@ -378,7 +384,7 @@ o.spec("route", function() { }, FRAME_BUDGET) }) - o("calls onmatch and view correct number of times", function() { + o("calls onmatch and view correct number of times", function(done) { var matchCount = 0 var renderCount = 0 var Component = { @@ -390,9 +396,9 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { "/" : { - onmatch: function(resolve) { + onmatch: function() { matchCount++ - resolve(Component) + return Component }, render: function(vnode) { renderCount++ @@ -401,13 +407,52 @@ o.spec("route", function() { }, }) - o(matchCount).equals(1) - o(renderCount).equals(1) + setTimeout(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) - redrawService.redraw() + redrawService.redraw() - o(matchCount).equals(1) - o(renderCount).equals(2) + o(matchCount).equals(1) + o(renderCount).equals(2) + + done() + }, 20) + }) + + o("calls onmatch and view correct number of times when not onmatch returns undefined", function(done) { + var matchCount = 0 + var renderCount = 0 + var Component = { + view: function() { + return m("div") + } + } + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + onmatch: function() { + matchCount++ + }, + render: function(vnode) { + renderCount++ + return {tag: Component} + }, + }, + }) + + setTimeout(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) + + redrawService.redraw() + + o(matchCount).equals(1) + o(renderCount).equals(2) + + done() + }, 20) }) o("onmatch can redirect to another route", function(done) { @@ -458,38 +503,11 @@ o.spec("route", function() { }, FRAME_BUDGET) }) - o("onmatch resolution callback resolves at most once", function(done) { - var resolveCount = 0 - var resolvedComponent - var A = {view: function() {}} - var B = {view: function() {}} - var C = {view: function() {}} - - $window.location.href = prefix + "/" - route(root, "/", { - "/": { - onmatch: function(resolve) { - resolve(A) - resolve(B) - callAsync(function() {resolve(C)}) - }, - render: function(vnode) { - resolveCount++ - resolvedComponent = vnode.tag - } - }, - }) - setTimeout(function() { - o(resolveCount).equals(1) - o(resolvedComponent).equals(A) - - done() - }, FRAME_BUDGET) - }) - o("the previous view redraws while onmatch resolution is pending (#1268)", function(done) { var view = o.spy() - var onmatch = o.spy() + var onmatch = o.spy(function() { + return new Promise(function() {}) + }) $window.location.href = prefix + "/a" route(root, "/", { @@ -516,7 +534,7 @@ o.spec("route", function() { }) o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ - var onmatch = o.spy(function(resolve) {resolve()}) + var onmatch = o.spy() var render = o.spy(function(){return m("div")}) $window.location.href = prefix + "/" @@ -527,16 +545,18 @@ o.spec("route", function() { } }) - o(onmatch.callCount).equals(1) - o(render.callCount).equals(1) - - route.set(route.get()) - setTimeout(function() { - o(onmatch.callCount).equals(2) - o(render.callCount).equals(2) + o(onmatch.callCount).equals(1) + o(render.callCount).equals(1) - done() + route.set(route.get()) + + setTimeout(function() { + o(onmatch.callCount).equals(2) + o(render.callCount).equals(2) + + done() + }, FRAME_BUDGET) }, FRAME_BUDGET) }) @@ -544,8 +564,12 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { - "/": {view: function(){}}, - "/2": {onmatch: function(){}} + "/": {view: function() {}}, + "/2": { + onmatch: function() { + return new Promise(function() {}) + } + } }) @@ -596,8 +620,10 @@ o.spec("route", function() { $window.location.href = prefix + "/a" route(root, "/a", { "/a": { - onmatch: function(resolve) { - setTimeout(resolve, 20) + onmatch: function() { + return new Promise(function(resolve) { + setTimeout(resolve, 20) + }) }, render: function(vnode) {resolved = "a"} }, diff --git a/docs/route.md b/docs/route.md index b16d1336..9089773e 100644 --- a/docs/route.md +++ b/docs/route.md @@ -107,18 +107,17 @@ A RouterResolver is an object that contains an `onmatch` method and/or a `render ##### routeResolver.onmatch -The `onmatch` hook is called when the router needs to find a component to render. It is called once when a router path changes, but not on subsequent redraws. It can be used to run logic before a component initializes (for example authentication logic) +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 or analytics tracking) -This method also allows you to asynchronously define what component will be rendered, making it suitable for code splitting and asynchronous module loading. +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. -`routeResolver.onmatch(resolve, args, requestedPath)` +`routeResolver.onmatch(args, requestedPath)` -Argument | Type | Description ---------------- | ------------------------ | --- -`resolve` | `Component -> undefined` | Call this function with a component as the first argument to use it as the route's component -`args` | `Object` | The [routing parameters](#routing-parameters) -`requestedPath` | `String` | The 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. -**returns** | | Returns `undefined` +Argument | Type | Description +--------------- | ------------------------------ | --- +`args` | `Object` | The [routing parameters](#routing-parameters) +`requestedPath` | `String` | The 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. +**returns** | `Promise|undefined` | Returns a promise that resolves to a component, or undefined ##### routeResolver.render @@ -286,8 +285,8 @@ Instead of mapping a component to a route, you can specify a RouteResolver objec ```javascript m.route(document.body, "/", { "/": { - onmatch: function(resolve, args, requestedPath) { - resolve(Home) + onmatch: function(args, requestedPath) { + return Home }, render: function(vnode) { return vnode // equivalent to m(Home) @@ -366,10 +365,12 @@ var Login = { m.route(document.body, "/secret", { "/secret": { - onmatch: function(resolve) { - if (isLoggedIn) resolve(Home) - else m.route.set("/login") + onmatch: function() { + if (!isLoggedIn) m.route.set("/login") }, + render: function() { + return m(Home) + } }, "/login": Login }) @@ -401,21 +402,20 @@ module.export = { ```javascript // index.js -function load(file, done) { - m.request({ +function load(file) { + return m.request({ method: "GET", url: file, extract: function(xhr) { return new Function("var module = {};" + xhr.responseText + ";return module.exports;") } }) - .run(done) } m.route(document.body, "/", { "/": { - onmatch: function(resolve) { - load("Home.js", resolve) + onmatch: function() { + return load("Home.js") }, }, }) @@ -428,9 +428,11 @@ Fortunately, there are a number of tools that facilitate the task of bundling mo ```javascript m.route(document.body, "/", { "/": { - onmatch: function(resolve) { + onmatch: function() { // using Webpack async code splitting - require(['./Home.js'], resolve) + return new Promise(function(resolve) { + require(['./Home.js'], resolve) + }) }, }, }) diff --git a/mithril.js b/mithril.js index 03017df3..a473784a 100644 --- a/mithril.js +++ b/mithril.js @@ -995,12 +995,12 @@ var coreRouter = function($window) { return data } var asyncId - function debounceAsync(f) { + function debounceAsync(callback0) { return function() { if (asyncId != null) return asyncId = callAsync0(function() { asyncId = null - f() + callback0() }) } } @@ -1044,7 +1044,7 @@ var coreRouter = function($window) { if (supportsPushState) { if (options && options.replace) $window.history.replaceState(null, null, prefix1 + path) else $window.history.pushState(null, null, prefix1 + path) - $window.onpopstate(true) + $window.onpopstate() } else $window.location.href = prefix1 + path } @@ -1090,7 +1090,6 @@ var coreRouter = function($window) { } var _20 = function($window, redrawService0) { var routeService = coreRouter($window) - var identity = function(v) {return v} var resolver, component, attrs3, currentPath, resolve var route = function(root, defaultRoute, routes) { @@ -1101,7 +1100,7 @@ var _20 = function($window, redrawService0) { render1() } var render1 = function() { - if (resolver != null) redrawService0.render(root, resolver.render(Vnode(component, attrs3.key, attrs3))) + if (resolver != null) redrawService0.render(root, resolver.render(Vnode(component || "div", attrs3.key, attrs3))) } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) @@ -1112,9 +1111,9 @@ var _20 = function($window, redrawService0) { resolve = function(resolved) { update(payload, resolved, params, path) } - payload.onmatch(function(resolved) { + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { if (resolve != null) resolve(resolved) - }, params, path) + }) } } else update(payload, "div", params, path) @@ -1131,9 +1130,9 @@ var _20 = function($window, redrawService0) { return route } m.route = _20(window, redrawService) -m.withAttr = function(attrName, callback0, context) { +m.withAttr = function(attrName, callback1, context) { return function(e) { - return callback0.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } var _27 = coreRenderer(window) diff --git a/mithril.min.js b/mithril.min.js index c64fe7e3..0b8fec4e 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -34,8 +34,8 @@ null==a.vnodes&&(a.textContent="");b instanceof Array||(b=[b]);g(a,a.vnodes,r.no K.setCompletionCallback(H.redraw);y.mount=function(a){return function(c,h){if(null===h)a.render(c,[]),a.unsubscribe(c);else{if(null==h.view)throw Error("m.mount(element, component) expects a component, not a vnode");a.subscribe(c,function(){a.render(c,r(h))});a.redraw()}}}(H);var L=function(a){if(""===a||null==a)return{};"?"===a.charAt(0)&&(a=a.slice(1));a=a.split("&");for(var c={},h={},d=0;d + From 846d6ce24b380bea12face9389a059583034d179 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 6 Dec 2016 10:44:25 -0500 Subject: [PATCH 07/37] router fixes --- api/router.js | 1 + docs/route.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/router.js b/api/router.js index c5911b90..1eace6b0 100644 --- a/api/router.js +++ b/api/router.js @@ -1,6 +1,7 @@ "use strict" var Vnode = require("../render/vnode") +var Promise = require("../promise/promise") var coreRouter = require("../router/router") module.exports = function($window, redrawService) { diff --git a/docs/route.md b/docs/route.md index 234bd678..0d840b8a 100644 --- a/docs/route.md +++ b/docs/route.md @@ -384,9 +384,9 @@ For the sake of simplicity, in the example above, the user's logged in status is ### 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 calling the `resolve` callback of the `onmatch` hook asynchronously: +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 simplest form, one could do the following: +At its most basic form, one could do the following: ```javascript // Home.js From 6a63cb702d7c44fa107fd8dbaa5f7957fb1eae31 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Tue, 6 Dec 2016 15:45:34 +0000 Subject: [PATCH 08/37] Bundled output for commit 846d6ce24b380bea12face9389a059583034d179 [skip ci] --- mithril.js | 5 +++-- mithril.min.js | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/mithril.js b/mithril.js index a473784a..9baf3a09 100644 --- a/mithril.js +++ b/mithril.js @@ -954,6 +954,7 @@ var _16 = function(redrawService0) { } } m.mount = _16(redrawService) +var Promise = PromisePolyfill var parseQueryString = function(string) { if (string === "" || string == null) return {} if (string.charAt(0) === "?") string = string.slice(1) @@ -1135,8 +1136,8 @@ m.withAttr = function(attrName, callback1, context) { return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } -var _27 = coreRenderer(window) -m.render = _27.render +var _28 = coreRenderer(window) +m.render = _28.render m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp diff --git a/mithril.min.js b/mithril.min.js index 0b8fec4e..65d7de53 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,21 +1,21 @@ -new function(){function r(a,c,h,d,g,l){return{tag:a,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function y(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var c,h,d=[],g={};c=P.exec(a);){var l=c[1],m=c[2];""===l&&""!==m?h=m:"#"===l?g.id=m:"."===l?d.push(m):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function m(a){try{return""!==a?JSON.parse(a):null}catch(M){throw Error(a);}}function A(a){return a.responseText}function n(a,c){if("function"=== -typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(n(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?u:v(u)},jsonp:function(b,k){var m=h();b=d(b,k);var u=new c(function(c,d){var k=b.callbackName|| -"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,h=a.document.createElement("script");a[k]=function(d){h.parentNode.removeChild(h);c(n(b.type,d));delete a[k]};h.onerror=function(){h.parentNode.removeChild(h);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=k;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?u:m(u)},setCompletionCallback:function(a){k=a}}}(window,"undefined"!==typeof Promise? -Promise:t),O=function(a){function c(e,f,a,b,c,d,g){for(;aa.indexOf("?")?"?":"&";a+=d+b}return a}function m(a){try{return""!==a?JSON.parse(a):null}catch(O){throw Error(a);}}function A(a){return a.responseText}function n(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(n(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?u:v(u)},jsonp:function(b,k){var m=h();b=d(b,k);var u=new c(function(c, +d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,h=a.document.createElement("script");a[k]=function(d){h.parentNode.removeChild(h);c(n(b.type,d));delete a[k]};h.onerror=function(){h.parentNode.removeChild(h);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=k;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?u:m(u)},setCompletionCallback:function(a){k=a}}}(window, +H),Q=function(a){function c(e,f,a,b,c,d,g){for(;a=w&&v>=u;){var x=f[w],p=a[u];if(x!==p||q)if(null==x)w++;else if(null==p)u++;else if(x.key===p.key)w++,u++,l(e,x,p,b,A(f,w,d),q,g),q&&x.tag===p.tag&&n(e,m(x),d);else if(x=f[z],x!==p||q)if(null==x)z--;else if(null==p)u++;else if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),(q||u=w&&v>=u;){x=f[z];p=a[v];if(x!==p||q)if(null==x)z--;else{if(null!= p)if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),q&&x.tag===p.tag&&n(e,m(x),d),null!=x.dom&&(d=x.dom),z--;else{if(!D){D=f;var x=z,r={},C;for(C=0;C Date: Tue, 6 Dec 2016 16:48:43 +0000 Subject: [PATCH 09/37] Document m.deferred removal --- docs/change-log.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index ea2ee425..0475c996 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -27,6 +27,7 @@ If you are migrating, consider using the [mithril-codemods](https://www.npmjs.co - [Accessing route params](#accessing-route-params) - [Preventing unmounting](#preventing-unmounting) - [`m.request`](#mrequest) +- [`m.deferred` removed](#mdeferred-removed) - [`m.sync` removed](#msync-removed) - [`xlink` namespace required](#xlink-namespace-required) - [Nested arrays in views](#nested-arrays-in-views) @@ -502,9 +503,45 @@ Additionally, if the `extract` option is passed to `m.request` the return value --- +## `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](promises.md) in non-supporting environments. In situations where you would have used `m.deferred`, you should use Promises instead. + +### `v0.2.x` + +```javascript +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` + +```javascript +var greetAsync = new Promise(function(resolve){ + setTimeout(function() { + resolve("hello"); + }, 1000); +}); + +greetAsync() + .then(function(value) {return value + " world"}) + .then(function(value) {console.log(value)}); //logs "hello world" after 1 second +``` + +--- + ## `m.sync` removed -`m.sync` has been removed in favor of `Promise.all` +Since `v1.x` uses standards-compliant Promises, `m.sync` is redundant. Use `Promise.all` instead. ### `v0.2.x` @@ -570,7 +607,7 @@ If a vnode is strictly equal to the vnode occupying its place in the last draw, ## `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 +They are considered anti-patterns and have a number of problematic edge cases, so they no longer exist in v1.x. --- From b6c79f591136be960aafb08340bc25eb859f645c Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 6 Dec 2016 12:43:23 -0500 Subject: [PATCH 10/37] tighten up async testing --- api/tests/test-router.js | 142 +++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 57 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 5ea27511..cb0e3a8c 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -5,6 +5,7 @@ var callAsync = require("../../test-utils/callAsync") var browserMock = require("../../test-utils/browserMock") var m = require("../../render/hyperscript") +var callAsync = require("../../test-utils/callAsync") var coreRenderer = require("../../render/render") var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") @@ -74,7 +75,7 @@ o.spec("route", function() { } }) - setTimeout(function() { + callAsync(function() { o(root.firstChild.nodeName).equals("DIV") $window.history.back() @@ -82,7 +83,7 @@ o.spec("route", function() { o($window.location.pathname).equals("/") done() - }, FRAME_BUDGET) + }) }) o("default route does not inherit params", function(done) { @@ -157,11 +158,11 @@ o.spec("route", function() { o(onclick.args[0].target).equals(root.firstChild) // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { + callAsync(function() { o(onupdate.callCount).equals(1) done() - }, FRAME_BUDGET * 2) + }) }) o("event handlers can skip redraw", function(done) { @@ -192,11 +193,11 @@ o.spec("route", function() { o(oninit.callCount).equals(1) // Wrapped to ensure no redraw fired - setTimeout(function() { + callAsync(function() { o(onupdate.callCount).equals(0) done() - }, FRAME_BUDGET) + }) }) o("changes location on route.link", function() { @@ -263,12 +264,12 @@ o.spec("route", function() { "/:id" : resolver }) - setTimeout(function() { + callAsync(function() { o(matchCount).equals(1) o(renderCount).equals(1) o(root.firstChild.nodeName).equals("DIV") done() - }, 20) + }) }) o("accepts RouteResolver without `render` method as payload", function(done) { @@ -293,20 +294,18 @@ o.spec("route", function() { }, }) - setTimeout(function() { + callAsync(function() { o(matchCount).equals(1) o(root.firstChild.nodeName).equals("DIV") done() - }, 20) + }) }) o("changing `vnode.key` in `render` resets the component", function(done, timeout){ - timeout(FRAME_BUDGET * 6) - var oninit = o.spy() var Component = { oninit: oninit, - view: function(){ + view: function() { return m("div") } } @@ -316,14 +315,14 @@ o.spec("route", function() { return m(Component, {key: vnode.attrs.id}) }} }) - setTimeout(function(){ + callAsync(function() { o(oninit.callCount).equals(1) route.set('/def') - setTimeout(function(){ + callAsync(function() { o(oninit.callCount).equals(2) done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) + }) + }) }) o("accepts RouteResolver without `onmatch` method as payload", function() { @@ -377,11 +376,11 @@ o.spec("route", function() { route.set("/b") - setTimeout(function() { + callAsync(function() { o(root.firstChild).equals(dom) done() - }, FRAME_BUDGET) + }) }) o("calls onmatch and view correct number of times", function(done) { @@ -407,7 +406,7 @@ o.spec("route", function() { }, }) - setTimeout(function() { + callAsync(function() { o(matchCount).equals(1) o(renderCount).equals(1) @@ -417,7 +416,7 @@ o.spec("route", function() { o(renderCount).equals(2) done() - }, 20) + }) }) o("calls onmatch and view correct number of times when not onmatch returns undefined", function(done) { @@ -442,7 +441,7 @@ o.spec("route", function() { }, }) - setTimeout(function() { + callAsync(function() { o(matchCount).equals(1) o(renderCount).equals(1) @@ -452,7 +451,7 @@ o.spec("route", function() { o(renderCount).equals(2) done() - }, 20) + }) }) o("onmatch can redirect to another route", function(done) { @@ -472,14 +471,39 @@ o.spec("route", function() { } }) - setTimeout(function() { + callAsync(function() { o(redirected).equals(true) done() - }, FRAME_BUDGET) + }) }) - o("onmatch can redirect to another route that has RouteResolver", function(done) { + o("onmatch can redirect to another route that has RouteResolver w/ only onmatch", function(done) { + var redirected = false + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + route.set("/b") + } + }, + "/b" : { + onmatch: function() { + redirected = true + return {view: function() {}} + } + } + }) + + callAsync(function() { + o(redirected).equals(true) + + done() + }) + }) + + o("onmatch can redirect to another route that has RouteResolver w/ only render", function(done) { var redirected = false $window.location.href = prefix + "/a" @@ -496,11 +520,11 @@ o.spec("route", function() { } }) - setTimeout(function() { + callAsync(function() { o(redirected).equals(true) done() - }, FRAME_BUDGET) + }) }) o("the previous view redraws while onmatch resolution is pending (#1268)", function(done) { @@ -520,7 +544,7 @@ o.spec("route", function() { route.set("/b") - setTimeout(function(){ + callAsync(function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) @@ -530,12 +554,12 @@ o.spec("route", function() { o(onmatch.callCount).equals(1) done() - }, FRAME_BUDGET) + }) }) o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ var onmatch = o.spy() - var render = o.spy(function(){return m("div")}) + var render = o.spy(function() {return m("div")}) $window.location.href = prefix + "/" route(root, '/', { @@ -545,19 +569,21 @@ o.spec("route", function() { } }) - setTimeout(function() { + callAsync(function() { o(onmatch.callCount).equals(1) o(render.callCount).equals(1) route.set(route.get()) - setTimeout(function() { - o(onmatch.callCount).equals(2) - o(render.callCount).equals(2) + callAsync(function() { + callAsync(function() { + o(onmatch.callCount).equals(2) + o(render.callCount).equals(2) - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) + done() + }) + }) + }) }) o("m.route.get() returns the last fully resolved route (#1276)", function(done){ @@ -577,10 +603,10 @@ o.spec("route", function() { route.set("/2") - setTimeout(function(){ + callAsync(function() { o(route.get()).equals("/") done() - }, FRAME_BUDGET) + }) }) o("routing with RouteResolver works more than once", function(done, timeout) { @@ -602,33 +628,35 @@ o.spec("route", function() { route.set('/b') - setTimeout(function(){ + callAsync(function() { route.set('/a') - setTimeout(function(){ + callAsync(function() { o(root.firstChild.nodeName).equals("A") done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) + }) + }) }) - o("calling route.set invalidates pending onmatch resolution", function(done, timeout) { - timeout(200) - + o("calling route.set invalidates pending onmatch resolution", function(done) { var resolved $window.location.href = prefix + "/a" route(root, "/a", { "/a": { onmatch: function() { - return new Promise(function(resolve) { - setTimeout(resolve, 20) + return Promise.resolve(function(resolve) { + setTimeout(resolve, 0) }) }, - render: function(vnode) {resolved = "a"} + render: function(vnode) { + resolved = "a" + } }, "/b": { - view: function() {resolved = "b"} + view: function() { + resolved = "b" + } } }) @@ -638,10 +666,10 @@ o.spec("route", function() { o(resolved).equals("b") done() - }, 30) + }, 2) //FIXME magic number }) - o("route changes activate onbeforeremove", function(done, timeout) { + o("route changes activate onbeforeremove", function(done) { var spy = o.spy() $window.location.href = prefix + "/a" @@ -657,11 +685,11 @@ o.spec("route", function() { route.set("/b") - setTimeout(function() { + callAsync(function() { o(spy.callCount).equals(1) done() - }, 30) + }) }) o("throttles", function(done, timeout) { @@ -680,12 +708,12 @@ o.spec("route", function() { redrawService.redraw() var after = i - setTimeout(function(){ + setTimeout(function() { o(before).equals(1) // routes synchronously o(after).equals(2) // redraws synchronously o(i).equals(3) // throttles rest done() - },40) + }, FRAME_BUDGET * 2) }) }) }) From 22f07edd52f66d2b354d1e06c3ec5e50c4ffbd8c Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Tue, 6 Dec 2016 18:18:53 +0000 Subject: [PATCH 11/37] Make ospec report the number of failing assertions --- ospec/ospec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 60c0b66c..f094dfe6 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -202,7 +202,10 @@ module.exports = new function init() { status = 1 } } - console.log(results.length + " assertions completed in " + Math.round(new Date - start) + "ms") + console.log( + results.length + " assertions completed in " + Math.round(new Date - start) + "ms, " + + "of which " + results.filter(function(result){return result.error}).length + " failed" + ) if (hasProcess && status === 1) process.exit(1) } From cf12c00fa1445efbb033c88c5d89090a059b7608 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Tue, 6 Dec 2016 21:34:24 +0100 Subject: [PATCH 12/37] [router] don't mutate RouteResolvers, move a branch out of redraw --- api/router.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/router.js b/api/router.js index 1eace6b0..d1452782 100644 --- a/api/router.js +++ b/api/router.js @@ -8,16 +8,16 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var resolver, component, attrs, currentPath, resolve + var render, component, attrs, currentPath, resolve var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - resolver = routeResolver, component = comp, attrs = params, currentPath = path, resolve = null - resolver.render = routeResolver.render || identity - render() + component = comp || "div", attrs = params, currentPath = path, resolve = null + render = (routeResolver.render || identity).bind(routeResolver) + run() } - var render = function() { - if (resolver != null) redrawService.render(root, resolver.render(Vnode(component || "div", attrs.key, attrs))) + var run = function() { + if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) @@ -38,7 +38,7 @@ module.exports = function($window, redrawService) { }, function() { routeService.setPath(defaultRoute) }) - redrawService.subscribe(root, render) + redrawService.subscribe(root, run) } route.set = routeService.setPath route.get = function() {return currentPath} From 650f7c4f3b7dce3e32b5e6795c066d090026eb7a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Tue, 6 Dec 2016 21:44:49 +0100 Subject: [PATCH 13/37] [router] remove logic made redundant by promise use --- api/router.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/api/router.js b/api/router.js index d1452782..04b5d091 100644 --- a/api/router.js +++ b/api/router.js @@ -8,11 +8,11 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render, component, attrs, currentPath, resolve + var render, component, attrs, currentPath var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp || "div", attrs = params, currentPath = path, resolve = null + component = comp || "div", attrs = params, currentPath = path render = (routeResolver.render || identity).bind(routeResolver) run() } @@ -23,15 +23,9 @@ module.exports = function($window, redrawService) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (resolve != null) update(payload, component, params, path) - else { - resolve = function(resolved) { - update(payload, resolved, params, path) - } - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (resolve != null) resolve(resolved) - }) - } + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + update(payload, resolved, params, path) + }) } else update(payload, "div", params, path) } From e73f4ba126e30030ab17554142203f82e76eaabf Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Tue, 6 Dec 2016 21:28:09 +0000 Subject: [PATCH 14/37] Bundled output for commit 678a8b13ae80c87c04f9dd416ade7b8011981fd3 [skip ci] --- README.md | 2 +- mithril.js | 26 ++++++---------- mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index ef729c3b..9ddad4bf 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.44 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.41 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 9baf3a09..64a76fb6 100644 --- a/mithril.js +++ b/mithril.js @@ -1092,37 +1092,31 @@ var coreRouter = function($window) { var _20 = function($window, redrawService0) { var routeService = coreRouter($window) var identity = function(v) {return v} - var resolver, component, attrs3, currentPath, resolve + var render1, component, attrs3, currentPath var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - resolver = routeResolver, component = comp, attrs3 = params, currentPath = path, resolve = null - resolver.render = routeResolver.render || identity - render1() + component = comp || "div", attrs3 = params, currentPath = path + render1 = (routeResolver.render || identity).bind(routeResolver) + run1() } - var render1 = function() { - if (resolver != null) redrawService0.render(root, resolver.render(Vnode(component || "div", attrs3.key, attrs3))) + var run1 = function() { + if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (resolve != null) update(payload, component, params, path) - else { - resolve = function(resolved) { - update(payload, resolved, params, path) - } - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (resolve != null) resolve(resolved) - }) - } + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + update(payload, resolved, params, path) + }) } else update(payload, "div", params, path) } }, function() { routeService.setPath(defaultRoute) }) - redrawService0.subscribe(root, render1) + redrawService0.subscribe(root, run1) } route.set = routeService.setPath route.get = function() {return currentPath} diff --git a/mithril.min.js b/mithril.min.js index 65d7de53..c0f2a5b2 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function r(a,c,h,d,g,l){return{tag:a,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function y(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===K[a]){for(var c,h,d=[],g={};c=R.exec(a);){var l=c[1],m=c[2];""===l&&""!==m?h=m:"#"===l?g.id=m:"."===l?d.push(m):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function m(a){try{return""!==a?JSON.parse(a):null}catch(O){throw Error(a);}}function A(a){return a.responseText}function n(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(n(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?u:v(u)},jsonp:function(b,k){var m=h();b=d(b,k);var u=new c(function(c, -d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,h=a.document.createElement("script");a[k]=function(d){h.parentNode.removeChild(h);c(n(b.type,d));delete a[k]};h.onerror=function(){h.parentNode.removeChild(h);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=k;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?u:m(u)},setCompletionCallback:function(a){k=a}}}(window, -H),Q=function(a){function c(e,f,a,b,c,d,g){for(;a=w&&v>=u;){var x=f[w],p=a[u];if(x!==p||q)if(null==x)w++;else if(null==p)u++;else if(x.key===p.key)w++,u++,l(e,x,p,b,A(f,w,d),q,g),q&&x.tag===p.tag&&n(e,m(x),d);else if(x=f[z],x!==p||q)if(null==x)z--;else if(null==p)u++;else if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),(q||u=w&&v>=u;){x=f[z];p=a[v];if(x!==p||q)if(null==x)z--;else{if(null!= -p)if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),q&&x.tag===p.tag&&n(e,m(x),d),null!=x.dom&&(d=x.dom),z--;else{if(!D){D=f;var x=z,r={},C;for(C=0;Ca.indexOf("?")?"?":"&";a+=d+b}return a}function t(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?k:u(k)},jsonp:function(b,x){var t=h();b=d(b,x);var k=new c(function(c, +d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,h=a.document.createElement("script");a[k]=function(d){h.parentNode.removeChild(h);c(p(b.type,d));delete a[k]};h.onerror=function(){h.parentNode.removeChild(h);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=k;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?k:t(k)},setCompletionCallback:function(a){x=a}}}(window, +I),P=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&u>=z;){var v=f[k],m=a[z];if(v!==m||r)if(null==v)k++;else if(null==m)z++;else if(v.key===m.key)k++,z++,l(e,v,m,b,n(f,k,d),r,g),r&&v.tag===m.tag&&p(e,t(v),d);else if(v=f[w],v!==m||r)if(null==v)w--;else if(null==m)z++;else if(v.key===m.key)l(e,v,m,b,n(f,w+1,d),r,g),(r||z=k&&u>=z;){v=f[w];m=a[u];if(v!==m||r)if(null==v)w--;else{if(null!= +m)if(v.key===m.key)l(e,v,m,b,n(f,w+1,d),r,g),r&&v.tag===m.tag&&p(e,t(v),d),null!=v.dom&&(d=v.dom),w--;else{if(!G){G=f;var v=w,B={},C;for(C=0;C Date: Tue, 6 Dec 2016 16:53:16 -0500 Subject: [PATCH 15/37] fix invalidation test --- api/tests/test-router.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index cb0e3a8c..7c93438a 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -640,16 +640,22 @@ o.spec("route", function() { }) o("calling route.set invalidates pending onmatch resolution", function(done) { + var rendered = false var resolved $window.location.href = prefix + "/a" route(root, "/a", { "/a": { onmatch: function() { - return Promise.resolve(function(resolve) { - setTimeout(resolve, 0) + return new Promise(function(resolve) { + callAsync(function() { + callAsync(function() { + resolve({view: function() {}}) + }) + }) }) }, render: function(vnode) { + rendered = true resolved = "a" } }, @@ -662,11 +668,12 @@ o.spec("route", function() { route.set("/b") - setTimeout(function() { + callAsync(function() { + o(rendered).equals(false) o(resolved).equals("b") done() - }, 2) //FIXME magic number + }) }) o("route changes activate onbeforeremove", function(done) { From c2acdf6f4adda63d36290d3c289df4a02d67fe15 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 6 Dec 2016 19:59:39 -0500 Subject: [PATCH 16/37] fix and test some edge cases in router --- api/router.js | 21 ++++-- api/tests/test-router.js | 150 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 158 insertions(+), 13 deletions(-) diff --git a/api/router.js b/api/router.js index 04b5d091..be13d8a4 100644 --- a/api/router.js +++ b/api/router.js @@ -8,11 +8,11 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render, component, attrs, currentPath + var routing = false, render, component, attrs, currentPath, resolve var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp || "div", attrs = params, currentPath = path + component = comp || "div", attrs = params, currentPath = path, resolve = null render = (routeResolver.render || identity).bind(routeResolver) run() } @@ -23,9 +23,15 @@ module.exports = function($window, redrawService) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - update(payload, resolved, params, path) - }) + if (resolve != null) update(payload, component, params, path) + else { + resolve = function(resolved) { + update(payload, resolved, params, path) + } + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + if (resolve != null) resolve(resolved) + }) + } } else update(payload, "div", params, path) } @@ -34,7 +40,10 @@ module.exports = function($window, redrawService) { }) redrawService.subscribe(root, run) } - route.set = routeService.setPath + route.set = function(path, data, options) { + resolve = null + routeService.setPath(path, data, options) + } route.get = function() {return currentPath} route.prefix = routeService.setPrefix route.link = routeService.link diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 7c93438a..6ac64be6 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -456,13 +456,15 @@ o.spec("route", function() { o("onmatch can redirect to another route", function(done) { var redirected = false - + var render = o.spy() + $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { onmatch: function() { route.set("/b") - } + }, + render: render }, "/b" : { view: function(vnode){ @@ -472,6 +474,7 @@ o.spec("route", function() { }) callAsync(function() { + o(render.callCount).equals(0) o(redirected).equals(true) done() @@ -480,13 +483,15 @@ o.spec("route", function() { o("onmatch can redirect to another route that has RouteResolver w/ only onmatch", function(done) { var redirected = false + var render = o.spy() $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { onmatch: function() { route.set("/b") - } + }, + render: render }, "/b" : { onmatch: function() { @@ -497,6 +502,7 @@ o.spec("route", function() { }) callAsync(function() { + o(render.callCount).equals(0) o(redirected).equals(true) done() @@ -505,13 +511,15 @@ o.spec("route", function() { o("onmatch can redirect to another route that has RouteResolver w/ only render", function(done) { var redirected = false + var render = o.spy() $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { onmatch: function() { route.set("/b") - } + }, + render: render }, "/b" : { render: function(vnode){ @@ -521,12 +529,101 @@ o.spec("route", function() { }) callAsync(function() { + o(render.callCount).equals(0) o(redirected).equals(true) done() }) }) + o("onmatch can redirect to a non-existent route that defaults to a RouteResolver w/ onmatch", function(done) { + var redirected = false + var render = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/b", { + "/a" : { + onmatch: function() { + route.set("/c") + }, + render: render + }, + "/b" : { + onmatch: function(vnode){ + redirected = true + return {view: function() {}} + } + } + }) + + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + + done() + }) + }) + }) + + o("onmatch can redirect to a non-existent route that defaults to a RouteResolver w/ render", function(done) { + var redirected = false + var render = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/b", { + "/a" : { + onmatch: function() { + route.set("/c") + }, + render: render + }, + "/b" : { + render: function(vnode){ + redirected = true + } + } + }) + + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + + done() + }) + }) + }) + + o("onmatch can redirect to a non-existent route that defaults to a component", function(done) { + var redirected = false + var render = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/b", { + "/a" : { + onmatch: function() { + route.set("/c") + }, + render: render + }, + "/b" : { + view: function(vnode){ + redirected = true + } + } + }) + + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + + done() + }) + }) + }) + o("the previous view redraws while onmatch resolution is pending (#1268)", function(done) { var view = o.spy() var onmatch = o.spy(function() { @@ -609,9 +706,7 @@ o.spec("route", function() { }) }) - o("routing with RouteResolver works more than once", function(done, timeout) { - timeout(200) - + o("routing with RouteResolver works more than once", function(done) { $window.location.href = prefix + "/a" route(root, '/a', { '/a': { @@ -676,6 +771,47 @@ o.spec("route", function() { }) }) + o("calling route.set invalidates pending onmatch resolution", function(done) { + var rendered = false + var resolved + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a": { + onmatch: function() { + return new Promise(function(resolve) { + callAsync(function() { + callAsync(function() { + resolve({view: function() {rendered = true}}) + }) + }) + }) + }, + render: function(vnode) { + rendered = true + resolved = "a" + } + }, + "/b": { + view: function() { + resolved = "b" + } + } + }) + + route.set("/b") + + callAsync(function() { + o(rendered).equals(false) + o(resolved).equals("b") + + callAsync(function() { + o(rendered).equals(false) + o(resolved).equals("b") + done() + }) + }) + }) + o("route changes activate onbeforeremove", function(done) { var spy = o.spy() From 0e9790e342e0d53704c89a0c16bee4702fd67f08 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 7 Dec 2016 01:00:36 +0000 Subject: [PATCH 17/37] Bundled output for commit c2acdf6f4adda63d36290d3c289df4a02d67fe15 [skip ci] --- README.md | 2 +- mithril.js | 21 +++++++++---- mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 9ddad4bf..4d6e53b3 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.41 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.46 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 64a76fb6..e3c8b8e0 100644 --- a/mithril.js +++ b/mithril.js @@ -1092,11 +1092,11 @@ var coreRouter = function($window) { var _20 = function($window, redrawService0) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render1, component, attrs3, currentPath + var routing = false, render1, component, attrs3, currentPath, resolve var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp || "div", attrs3 = params, currentPath = path + component = comp || "div", attrs3 = params, currentPath = path, resolve = null render1 = (routeResolver.render || identity).bind(routeResolver) run1() } @@ -1107,9 +1107,15 @@ var _20 = function($window, redrawService0) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - update(payload, resolved, params, path) - }) + if (resolve != null) update(payload, component, params, path) + else { + resolve = function(resolved) { + update(payload, resolved, params, path) + } + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + if (resolve != null) resolve(resolved) + }) + } } else update(payload, "div", params, path) } @@ -1118,7 +1124,10 @@ var _20 = function($window, redrawService0) { }) redrawService0.subscribe(root, run1) } - route.set = routeService.setPath + route.set = function(path, data, options) { + resolve = null + routeService.setPath(path, data, options) + } route.get = function() {return currentPath} route.prefix = routeService.setPrefix route.link = routeService.link diff --git a/mithril.min.js b/mithril.min.js index c0f2a5b2..cf44690d 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function m(a,c,h,d,g,l){return{tag:a,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function y(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===L[a]){for(var c,h,d=[],g={};c=Q.exec(a);){var l=c[1],t=c[2];""===l&&""!==t?h=t:"#"===l?g.id=t:"."===l?d.push(t):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function t(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?k:u(k)},jsonp:function(b,x){var t=h();b=d(b,x);var k=new c(function(c, -d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,h=a.document.createElement("script");a[k]=function(d){h.parentNode.removeChild(h);c(p(b.type,d));delete a[k]};h.onerror=function(){h.parentNode.removeChild(h);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=k;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?k:t(k)},setCompletionCallback:function(a){x=a}}}(window, -I),P=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&u>=z;){var v=f[k],m=a[z];if(v!==m||r)if(null==v)k++;else if(null==m)z++;else if(v.key===m.key)k++,z++,l(e,v,m,b,n(f,k,d),r,g),r&&v.tag===m.tag&&p(e,t(v),d);else if(v=f[w],v!==m||r)if(null==v)w--;else if(null==m)z++;else if(v.key===m.key)l(e,v,m,b,n(f,w+1,d),r,g),(r||z=k&&u>=z;){v=f[w];m=a[u];if(v!==m||r)if(null==v)w--;else{if(null!= -m)if(v.key===m.key)l(e,v,m,b,n(f,w+1,d),r,g),r&&v.tag===m.tag&&p(e,t(v),d),null!=v.dom&&(d=v.dom),w--;else{if(!G){G=f;var v=w,B={},C;for(C=0;Ca.indexOf("?")?"?":"&";a+=d+b}return a}function n(a){try{return""!==a?JSON.parse(a):null}catch(F){throw Error(a);}}function A(a){return a.responseText}function m(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bh.status||304===h.status)c(m(b.type,a));else{var g=Error(h.responseText),k;for(k in a)g[k]=a[k];d(g)}}catch(G){d(G)}};k&&null!=b.data?h.send(b.data):h.send()});return!0===b.background?t:u(t)},jsonp:function(b,h){var n=k();b=d(b,h);var t=new c(function(c, +d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,k=a.document.createElement("script");a[h]=function(d){k.parentNode.removeChild(k);c(m(b.type,d));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){h=a}}}(window, +J),Q=function(a){function c(e,f,a,b,c,d,h){for(;a=v&&u>=t;){var x=f[v],p=a[t];if(x!==p||q)if(null==x)v++;else if(null==p)t++;else if(x.key===p.key)v++,t++,l(e,x,p,b,A(f,v,d),q,g),q&&x.tag===p.tag&&m(e,n(x),d);else if(x=f[z],x!==p||q)if(null==x)z--;else if(null==p)t++;else if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),(q||t=v&&u>=t;){x=f[z];p=a[u];if(x!==p||q)if(null==x)z--;else{if(null!= +p)if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),q&&x.tag===p.tag&&m(e,n(x),d),null!=x.dom&&(d=x.dom),z--;else{if(!E){E=f;var x=z,r={},C;for(C=0;C Date: Tue, 6 Dec 2016 21:59:45 -0500 Subject: [PATCH 18/37] test some more routeresolver cases --- api/tests/test-router.js | 78 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 6ac64be6..1f3020b7 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -231,12 +231,12 @@ o.spec("route", function() { o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") }) - o("accepts RouteResolver", function(done) { + o("accepts RouteResolver with onmatch that returns Component", function(done) { var matchCount = 0 var renderCount = 0 var Component = { view: function() { - return m("div") + return m("span") } } @@ -267,7 +267,48 @@ o.spec("route", function() { callAsync(function() { o(matchCount).equals(1) o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.nodeName).equals("SPAN") + done() + }) + }) + + o("accepts RouteResolver with onmatch that returns Promise", function(done) { + var matchCount = 0 + var renderCount = 0 + var Component = { + view: function() { + return m("span") + } + } + + var resolver = { + onmatch: function(args, requestedPath) { + matchCount++ + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + o(this).equals(resolver) + return Promise.resolve(Component) + }, + render: function(vnode) { + renderCount++ + + o(vnode.attrs.id).equals("abc") + o(this).equals(resolver) + + return vnode + }, + } + + $window.location.href = prefix + "/abc" + route(root, "/abc", { + "/:id" : resolver + }) + + callAsync(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) + o(root.firstChild.nodeName).equals("SPAN") done() }) }) @@ -835,6 +876,37 @@ o.spec("route", function() { }) }) + o("asynchronous route.set in onmatch works", function(done) { + var rendered = false, resolved + route(root, "/a", { + "/a": { + onmatch: function() { + return Promise.resolve().then(function() { + route.set("/b") + }) + }, + render: function(vnode) { + rendered = true + resolved = "a" + } + }, + "/b": { + view: function() { + resolved = "b" + } + }, + }) + + callAsync(function() { + callAsync(function() { + o(rendered).equals(false) + o(resolved).equals("b") + + done() + }) + }) + }) + o("throttles", function(done, timeout) { timeout(200) From 992aa30ccc7b0678f4ff808a4c02477c1af1d64d Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 6 Dec 2016 23:29:15 -0500 Subject: [PATCH 19/37] fix m.route.link and m.route.set history replacement --- README.md | 2 +- api/router.js | 16 +++++- api/tests/test-router.js | 16 +++--- mithril.js | 56 ++++++++++---------- mithril.min.js | 82 ++++++++++++++--------------- router/router.js | 46 ++++++---------- router/tests/index.html | 1 - router/tests/test-defineRoutes.js | 4 +- router/tests/test-getPath.js | 2 +- router/tests/test-link.js | 87 ------------------------------- router/tests/test-setPath.js | 4 +- 11 files changed, 113 insertions(+), 203 deletions(-) delete mode 100644 router/tests/test-link.js diff --git a/README.md b/README.md index 4d6e53b3..11a87ed7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.46 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.47 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/api/router.js b/api/router.js index be13d8a4..915739a5 100644 --- a/api/router.js +++ b/api/router.js @@ -41,11 +41,23 @@ module.exports = function($window, redrawService) { redrawService.subscribe(root, run) } route.set = function(path, data, options) { + if (resolve != null) options = {replace: true} resolve = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} - route.prefix = routeService.setPrefix - route.link = routeService.link + route.prefix = function(prefix) {routeService.prefix = prefix} + route.link = function(vnode) { + vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href) + vnode.dom.onclick = function(e) { + if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return + e.preventDefault() + e.redraw = false + var href = this.getAttribute("href") + if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) + route.set(href, undefined, undefined) + } + } + return route } diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 1f3020b7..3de4ee54 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -358,7 +358,7 @@ o.spec("route", function() { }) callAsync(function() { o(oninit.callCount).equals(1) - route.set('/def') + route.set("/def") callAsync(function() { o(oninit.callCount).equals(2) done() @@ -897,12 +897,14 @@ o.spec("route", function() { }, }) - callAsync(function() { - callAsync(function() { - o(rendered).equals(false) - o(resolved).equals("b") - - done() + callAsync(function() { // tick for popstate for /a + callAsync(function() { // tick for promise in onmatch + callAsync(function() { // tick for onpopstate for /b + o(rendered).equals(false) + o(resolved).equals("b") + + done() + }) }) }) }) diff --git a/mithril.js b/mithril.js index e3c8b8e0..d4cb36b8 100644 --- a/mithril.js +++ b/mithril.js @@ -988,8 +988,6 @@ var parseQueryString = function(string) { var coreRouter = function($window) { var supportsPushState = typeof $window.history.pushState === "function" var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout - var prefix1 = "#!" - function setPrefix(value) {prefix1 = value} function normalize1(fragment0) { var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data @@ -1020,15 +1018,16 @@ var coreRouter = function($window) { } return path.slice(0, pathEnd) } - function getPath() { - var type2 = prefix1.charAt(0) + var router = {prefix: "#!"} + router.getPath = function() { + var type2 = router.prefix.charAt(0) switch (type2) { - case "#": return normalize1("hash").slice(prefix1.length) - case "?": return normalize1("search").slice(prefix1.length) + normalize1("hash") - default: return normalize1("pathname").slice(prefix1.length) + normalize1("search") + normalize1("hash") + case "#": return normalize1("hash").slice(router.prefix.length) + case "?": return normalize1("search").slice(router.prefix.length) + normalize1("hash") + default: return normalize1("pathname").slice(router.prefix.length) + normalize1("search") + normalize1("hash") } } - function setPath(path, data, options) { + router.setPath = function(path, data, options) { var queryData = {}, hashData = {} path = parsePath(path, queryData, hashData) if (data != null) { @@ -1043,15 +1042,15 @@ var coreRouter = function($window) { var hash = buildQueryString(hashData) if (hash) path += "#" + hash if (supportsPushState) { - if (options && options.replace) $window.history.replaceState(null, null, prefix1 + path) - else $window.history.pushState(null, null, prefix1 + path) + if (options && options.replace) $window.history.replaceState(null, null, router.prefix + path) + else $window.history.pushState(null, null, router.prefix + path) $window.onpopstate() } - else $window.location.href = prefix1 + path + else $window.location.href = router.prefix + path } - function defineRoutes(routes, resolve, reject) { + router.defineRoutes = function(routes, resolve, reject) { function resolveRoute() { - var path = getPath() + var path = router.getPath() var params = {} var pathname = parsePath(path, params, params) @@ -1073,21 +1072,11 @@ var coreRouter = function($window) { } if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) - else if (prefix1.charAt(0) === "#") $window.onhashchange = resolveRoute + else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute resolveRoute() } - function link(vnode2) { - vnode2.dom.setAttribute("href", prefix1 + vnode2.attrs.href) - vnode2.dom.onclick = function(e) { - if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return - e.preventDefault() - e.redraw = false - var href = this.getAttribute("href") - if (href.indexOf(prefix1) === 0) href = href.slice(prefix1.length) - setPath(href, undefined, undefined) - } - } - return {setPrefix: setPrefix, getPath: getPath, setPath: setPath, defineRoutes: defineRoutes, link: link} + + return router } var _20 = function($window, redrawService0) { var routeService = coreRouter($window) @@ -1125,12 +1114,23 @@ var _20 = function($window, redrawService0) { redrawService0.subscribe(root, run1) } route.set = function(path, data, options) { + if (resolve != null) options = {replace: true} resolve = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} - route.prefix = routeService.setPrefix - route.link = routeService.link + route.prefix = function(prefix0) {routeService.prefix = prefix0} + route.link = function(vnode1) { + vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href) + vnode1.dom.onclick = function(e) { + if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return + e.preventDefault() + e.redraw = false + var href = this.getAttribute("href") + if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) + route.set(href, undefined, undefined) + } + } return route } m.route = _20(window, redrawService) diff --git a/mithril.min.js b/mithril.min.js index cf44690d..af119c16 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function r(a,c,k,d,g,l){return{tag:a,key:c,attrs:k,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function y(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===M[a]){for(var c,k,d=[],g={};c=R.exec(a);){var l=c[1],n=c[2];""===l&&""!==n?k=n:"#"===l?g.id=n:"."===l?d.push(n):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function n(a){try{return""!==a?JSON.parse(a):null}catch(F){throw Error(a);}}function A(a){return a.responseText}function m(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bh.status||304===h.status)c(m(b.type,a));else{var g=Error(h.responseText),k;for(k in a)g[k]=a[k];d(g)}}catch(G){d(G)}};k&&null!=b.data?h.send(b.data):h.send()});return!0===b.background?t:u(t)},jsonp:function(b,h){var n=k();b=d(b,h);var t=new c(function(c, -d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,k=a.document.createElement("script");a[h]=function(d){k.parentNode.removeChild(k);c(m(b.type,d));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){h=a}}}(window, -J),Q=function(a){function c(e,f,a,b,c,d,h){for(;a=v&&u>=t;){var x=f[v],p=a[t];if(x!==p||q)if(null==x)v++;else if(null==p)t++;else if(x.key===p.key)v++,t++,l(e,x,p,b,A(f,v,d),q,g),q&&x.tag===p.tag&&m(e,n(x),d);else if(x=f[z],x!==p||q)if(null==x)z--;else if(null==p)t++;else if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),(q||t=v&&u>=t;){x=f[z];p=a[u];if(x!==p||q)if(null==x)z--;else{if(null!= -p)if(x.key===p.key)l(e,x,p,b,A(f,z+1,d),q,g),q&&x.tag===p.tag&&m(e,n(x),d),null!=x.dom&&(d=x.dom),z--;else{if(!E){E=f;var x=z,r={},C;for(C=0;Ca.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(G){d(G)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c, +d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window, +H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&t>=A;){var w=f[k],u=a[A];if(w!==u||q)if(null==w)k++;else if(null==u)A++;else if(w.key===u.key)k++,A++,m(e,w,u,b,n(f,k,d),q,g),q&&w.tag===u.tag&&p(e,l(w),d);else if(w=f[x],w!==u||q)if(null==w)x--;else if(null==u)A++;else if(w.key===u.key)m(e,w,u,b,n(f,x+1,d),q,g),(q||A=k&&t>=A;){w=f[x];u=a[t];if(w!==u||q)if(null==w)x--;else{if(null!= +u)if(w.key===u.key)m(e,w,u,b,n(f,x+1,d),q,g),q&&w.tag===u.tag&&p(e,l(w),d),null!=w.dom&&(d=w.dom),x--;else{if(!D){D=f;var w=x,C={},E;for(E=0;E - diff --git a/router/tests/test-defineRoutes.js b/router/tests/test-defineRoutes.js index b00530b7..51b3cc09 100644 --- a/router/tests/test-defineRoutes.js +++ b/router/tests/test-defineRoutes.js @@ -14,7 +14,7 @@ o.spec("Router.defineRoutes", function() { o.beforeEach(function() { $window = pushStateMock(env) router = new Router($window) - router.setPrefix(prefix) + router.prefix = prefix onRouteChange = o.spy() onFail = o.spy() }) @@ -73,7 +73,7 @@ o.spec("Router.defineRoutes", function() { $window.location.href = "file://" + prefix + "/test" router = new Router($window) - router.setPrefix(prefix) + router.prefix = prefix router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) diff --git a/router/tests/test-getPath.js b/router/tests/test-getPath.js index 119c1713..6c5dc2b2 100644 --- a/router/tests/test-getPath.js +++ b/router/tests/test-getPath.js @@ -13,7 +13,7 @@ o.spec("Router.getPath", function() { o.beforeEach(function() { $window = pushStateMock(env) router = new Router($window) - router.setPrefix(prefix) + router.prefix = prefix onRouteChange = o.spy() onFail = o.spy() }) diff --git a/router/tests/test-link.js b/router/tests/test-link.js deleted file mode 100644 index 3d07af2e..00000000 --- a/router/tests/test-link.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var renderService = require("../../render/render") -var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var domMock = require("../../test-utils/domMock") -var Router = require("../../router/router") - -o.spec("Router.link", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, dom, root, router, onRouteChange, onFail, render - - o.beforeEach(function() { - $window = pushStateMock(env) - dom = domMock() - root = dom.document.body - router = new Router($window) - router.setPrefix(prefix) - onRouteChange = o.spy() - onFail = o.spy() - render = renderService(dom).render - }) - - o("works", function(done) { - var A = { - view: function() { - return {tag: "a", attrs: {href: "/b", oncreate: router.link}} - } - } - var B = { - view: function() { - return {tag: "a", attrs: {href: "/a", oncreate: router.link}} - } - } - - $window.location.href = prefix + "/a" - router.defineRoutes({"/a": {tag: A}, "/b": {tag: B}}, function(component) { - render(root, component) - }) - - callAsync(function() { - var e = dom.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - callAsync(function() { - o(router.getPath()).equals("/b") - - done() - }) - }) - }) - - o("works after update", function(done) { - var id = "a" - var A = { - view: function() { - return {tag: "a", attrs: {href: "/" + id, oncreate: router.link}} - } - } - - $window.location.href = prefix + "/a" - router.defineRoutes({"/a": {tag: A}, "/b": {tag: A}}, function(component) { - render(root, {tag: A}) - id = "b" - render(root, {tag: A}) - }) - - callAsync(function() { - var e = dom.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - callAsync(function() { - o(router.getPath()).equals("/b") - - done() - }) - }) - }) - }) - }) - }) -}) diff --git a/router/tests/test-setPath.js b/router/tests/test-setPath.js index 6b817cfb..c957a59a 100644 --- a/router/tests/test-setPath.js +++ b/router/tests/test-setPath.js @@ -14,7 +14,7 @@ o.spec("Router.setPath", function() { o.beforeEach(function() { $window = pushStateMock(env) router = new Router($window) - router.setPrefix(prefix) + router.prefix = prefix onRouteChange = o.spy() onFail = o.spy() }) @@ -88,7 +88,7 @@ o.spec("Router.setPath", function() { $window.location.href = "file://" + prefix + "/test" router = new Router($window) - router.setPrefix(prefix) + router.prefix = prefix router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail) From f1f52445ec62a3a8b96a14ed5489f26c9887d59d Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 07:57:58 -0500 Subject: [PATCH 20/37] more router fixes --- api/router.js | 11 ++-- api/tests/test-router.js | 119 +++++++++++++++++++++++++++++++++++++++ docs/route.md | 69 +++++++++++++++++++++-- 3 files changed, 190 insertions(+), 9 deletions(-) diff --git a/api/router.js b/api/router.js index 915739a5..ade067ec 100644 --- a/api/router.js +++ b/api/router.js @@ -12,13 +12,16 @@ module.exports = function($window, redrawService) { var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp || "div", attrs = params, currentPath = path, resolve = null + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, resolve = null render = (routeResolver.render || identity).bind(routeResolver) run() } var run = function() { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } + var bail = function() { + routeService.setPath(defaultRoute) + } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) else { @@ -30,14 +33,12 @@ module.exports = function($window, redrawService) { } Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { if (resolve != null) resolve(resolved) - }) + }, bail) } } else update(payload, "div", params, path) } - }, function() { - routeService.setPath(defaultRoute) - }) + }, bail) redrawService.subscribe(root, run) } route.set = function(path, data, options) { diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 3de4ee54..8fc6c3bc 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -313,6 +313,125 @@ o.spec("route", function() { }) }) + o("accepts RouteResolver with onmatch that returns Promise", function(done) { + var matchCount = 0 + var renderCount = 0 + var Component = { + view: function() { + return m("span") + } + } + + var resolver = { + onmatch: function(args, requestedPath) { + matchCount++ + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + o(this).equals(resolver) + return Promise.resolve() + }, + render: function(vnode) { + renderCount++ + + o(vnode.attrs.id).equals("abc") + o(this).equals(resolver) + + return vnode + }, + } + + $window.location.href = prefix + "/abc" + route(root, "/abc", { + "/:id" : resolver + }) + + callAsync(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) + o(root.firstChild.nodeName).equals("DIV") + done() + }) + }) + + o("accepts RouteResolver with onmatch that returns Promise", function(done) { + var matchCount = 0 + var renderCount = 0 + var Component = { + view: function() { + return m("span") + } + } + + var resolver = { + onmatch: function(args, requestedPath) { + matchCount++ + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + o(this).equals(resolver) + return Promise.resolve([]) + }, + render: function(vnode) { + renderCount++ + + o(vnode.attrs.id).equals("abc") + o(this).equals(resolver) + + return vnode + }, + } + + $window.location.href = prefix + "/abc" + route(root, "/abc", { + "/:id" : resolver + }) + + callAsync(function() { + o(matchCount).equals(1) + o(renderCount).equals(1) + o(root.firstChild.nodeName).equals("DIV") + done() + }) + }) + + o("accepts RouteResolver with onmatch that returns rejected Promise", function(done) { + var matchCount = 0 + var renderCount = 0 + var spy = o.spy() + var Component = { + view: function() { + return m("span") + } + } + + var resolver = { + onmatch: function(args, requestedPath) { + matchCount++ + return Promise.reject(new Error("error")) + }, + render: function(vnode) { + renderCount++ + return vnode + }, + } + + $window.location.href = prefix + "/test/1" + route(root, "/default", { + "/default" : {view: spy}, + "/test/:id" : resolver + }) + + callAsync(function() { + callAsync(function() { + o(matchCount).equals(1) + o(renderCount).equals(0) + o(spy.callCount).equals(1) + done() + }) + }) + }) + o("accepts RouteResolver without `render` method as payload", function(done) { var matchCount = 0 var Component = { diff --git a/docs/route.md b/docs/route.md index 0d840b8a..e98a8070 100644 --- a/docs/route.md +++ b/docs/route.md @@ -18,6 +18,7 @@ - [Advanced component resolution](#advanced-component-resolution) - [Wrapping a layout component](#wrapping-a-layout-component) - [Authentication](#authentication) +- [Preloading data](#preloading-data) - [Code splitting](#code-splitting) --- @@ -107,7 +108,7 @@ A RouterResolver is an object that contains an `onmatch` method and/or a `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 or analytics tracking) +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. @@ -117,7 +118,11 @@ Argument | Type | Description --------------- | ------------------------------ | --- `args` | `Object` | The [routing parameters](#routing-parameters) `requestedPath` | `String` | The 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. -**returns** | `Promise|undefined` | Returns a promise that resolves to a component, or undefined +**returns** | `Component|Promise` | Returns 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`. ##### routeResolver.render @@ -127,8 +132,8 @@ The `render` method is called on every redraw for a matching route. It is simila Argument | Type | Description ------------------- | --------------- | ----------- -`vnode` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If the routeResolver does not have a `resolve` method, the vnode's `tag` field defaults to a `div` -`vnode.attrs` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If the routeResolver does not have a `resolve` method, the vnode defaults to a `div` +`vnode` | `Object` | A [vnode](vnodes.md) 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.attrs` | `Object` | A map of URL parameter values **returns** | `Vnode` | Returns a vnode --- @@ -382,6 +387,62 @@ For the sake of simplicity, in the example above, the user's logged in status is --- +### Preloading data + +Typically, a component can load data upon initialization. Loading data this way renders the component twice (once upon routing, and once after the request completes). + +```javascript +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: loadUsers, + view: function() { + return state.users.length > 0 ? state.users.map(function() { + 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: + +```javascript +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: loadUsers, + render: function() { + return state.users.length > 0 ? state.users.map(function() { + return m("div", user.id) + }) : "loading" + } + }, +}) +``` + +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: From 0a088f9f5ad58131c9388aec4aaa3cd2365c0330 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 7 Dec 2016 12:59:18 +0000 Subject: [PATCH 21/37] Bundled output for commit f1f52445ec62a3a8b96a14ed5489f26c9887d59d [skip ci] --- README.md | 2 +- mithril.js | 11 ++++--- mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 11a87ed7..4b900337 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.47 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.49 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index d4cb36b8..b190fda0 100644 --- a/mithril.js +++ b/mithril.js @@ -1085,13 +1085,16 @@ var _20 = function($window, redrawService0) { var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp || "div", attrs3 = params, currentPath = path, resolve = null + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, resolve = null render1 = (routeResolver.render || identity).bind(routeResolver) run1() } var run1 = function() { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } + var bail = function() { + routeService.setPath(defaultRoute) + } routeService.defineRoutes(routes, function(payload, params, path) { if (payload.view) update({}, payload, params, path) else { @@ -1103,14 +1106,12 @@ var _20 = function($window, redrawService0) { } Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { if (resolve != null) resolve(resolved) - }) + }, bail) } } else update(payload, "div", params, path) } - }, function() { - routeService.setPath(defaultRoute) - }) + }, bail) redrawService0.subscribe(root, run1) } route.set = function(path, data, options) { diff --git a/mithril.min.js b/mithril.min.js index af119c16..ea220c93 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function v(a,c,h,d,g,m){return{tag:a,key:c,attrs:h,children:d,text:g,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function z(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===K[a]){for(var c,h,d=[],g={};c=P.exec(a);){var m=c[1],l=c[2];""===m&&""!==l?h=l:"#"===m?g.id=l:"."===m?d.push(l):"["===c[3][0]&&((m=c[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(m):g[c[4]]=m||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(G){d(G)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c, -d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window, -H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&t>=A;){var w=f[k],u=a[A];if(w!==u||q)if(null==w)k++;else if(null==u)A++;else if(w.key===u.key)k++,A++,m(e,w,u,b,n(f,k,d),q,g),q&&w.tag===u.tag&&p(e,l(w),d);else if(w=f[x],w!==u||q)if(null==w)x--;else if(null==u)A++;else if(w.key===u.key)m(e,w,u,b,n(f,x+1,d),q,g),(q||A=k&&t>=A;){w=f[x];u=a[t];if(w!==u||q)if(null==w)x--;else{if(null!= -u)if(w.key===u.key)m(e,w,u,b,n(f,x+1,d),q,g),q&&w.tag===u.tag&&p(e,l(w),d),null!=w.dom&&(d=w.dom),x--;else{if(!D){D=f;var w=x,C={},E;for(E=0;Ea.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function n(a){return a.responseText}function m(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bz.status||304===z.status)c(m(b.type,a));else{var g=Error(z.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?z.send(b.data):z.send()});return!0===b.background?w:t(w)},jsonp:function(b,k){var n=h();b=d(b,k);var w=new c(function(c, +d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+p++,k=a.document.createElement("script");a[h]=function(d){k.parentNode.removeChild(k);c(m(b.type,d));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?w:n(w)},setCompletionCallback:function(a){t=a}}}(window, +H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=u&&w>=A;){var r=f[u],v=a[A];if(r!==v||q)if(null==r)u++;else if(null==v)A++;else if(r.key===v.key)u++,A++,l(e,r,v,b,n(f,u,d),q,g),q&&r.tag===v.tag&&m(e,k(r),d);else if(r=f[p],r!==v||q)if(null==r)p--;else if(null==v)A++;else if(r.key===v.key)l(e,r,v,b,n(f,p+1,d),q,g),(q||A=u&&w>=A;){r=f[p];v=a[w];if(r!==v||q)if(null==r)p--;else{if(null!= +v)if(r.key===v.key)l(e,r,v,b,n(f,p+1,d),q,g),q&&r.tag===v.tag&&m(e,k(r),d),null!=r.dom&&(d=r.dom),p--;else{if(!z){z=f;var r=p,D={},y;for(y=0;y Date: Wed, 7 Dec 2016 07:57:32 -0700 Subject: [PATCH 22/37] Update route docs to reference state var --- docs/route.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/route.md b/docs/route.md index e98a8070..0f8592cc 100644 --- a/docs/route.md +++ b/docs/route.md @@ -429,7 +429,7 @@ var state = { m.route(document.body, "/user/list", { "/user/list": { - onmatch: loadUsers, + onmatch: state.loadUsers, render: function() { return state.users.length > 0 ? state.users.map(function() { return m("div", user.id) From ae916664de9356d6d1a3e55791d2ddb93a786cc6 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 10:09:49 -0500 Subject: [PATCH 23/37] tweaks to docs --- docs/request.md | 2 +- docs/route.md | 2 +- docs/withAttr.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/request.md b/docs/request.md index cd204844..75be0de9 100644 --- a/docs/request.md +++ b/docs/request.md @@ -76,7 +76,7 @@ m.request({ }) ``` -Calls to `m.request` return a [promise](promise.md). +A call to `m.request` return a [promise](promise.md) and trigger a redraw upon completion of its promise chain. By default, `m.request` assumes the response is in JSON format and parses it into a Javascript object (or array). diff --git a/docs/route.md b/docs/route.md index e98a8070..f493276b 100644 --- a/docs/route.md +++ b/docs/route.md @@ -122,7 +122,7 @@ Argument | Type | Description 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`. +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 diff --git a/docs/withAttr.md b/docs/withAttr.md index 10638e51..ac000a22 100644 --- a/docs/withAttr.md +++ b/docs/withAttr.md @@ -15,7 +15,7 @@ Returns an event handler that runs `callback` with the value of the specified DO ```javascript var state = { value: "", - setValue: function(v) {value = v} + setValue: function(v) {state.value = v} } var Component = { From 7cab626980934b2e5b26e90c11d3c1d874ed3629 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Wed, 7 Dec 2016 13:32:18 +0100 Subject: [PATCH 24/37] Add a short browserMock test suite --- test-utils/tests/index.html | 2 ++ test-utils/tests/test-browserMock.js | 40 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 test-utils/tests/test-browserMock.js diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html index 4e5a6234..e24fa2f8 100644 --- a/test-utils/tests/index.html +++ b/test-utils/tests/index.html @@ -13,11 +13,13 @@ + + diff --git a/test-utils/tests/test-browserMock.js b/test-utils/tests/test-browserMock.js new file mode 100644 index 00000000..d47c58ce --- /dev/null +++ b/test-utils/tests/test-browserMock.js @@ -0,0 +1,40 @@ +"use strict" + +var o = require("../../ospec/ospec") +var browserMock = require("../../test-utils/browserMock") +var callAsync = require("../../test-utils/callAsync") +o.spec("browserMock", function() { + + var $window + o.beforeEach(function() { + $window = browserMock() + }) + + o("Mocks DOM, pushState and XHR", function() { + o($window.location).notEquals(undefined) + o($window.document).notEquals(undefined) + o($window.XMLHttpRequest).notEquals(undefined) + }) + o("$window.onhashchange can be reached from the pushStateMock functions", function(done) { + $window.onhashchange = o.spy() + $window.location.hash = '#a' + + callAsync(function(){ + o($window.onhashchange.callCount).equals(1) + done() + }) + }) + o("$window.onpopstate can be reached from the pushStateMock functions", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "#a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("$window.onunload can be reached from the pushStateMock functions", function() { + $window.onunload = o.spy() + $window.location.href = '/a' + + o($window.onunload.callCount).equals(1) + }) +}) From bd792979054ad7b27056312485368b7a4f34a434 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Wed, 7 Dec 2016 13:42:27 +0100 Subject: [PATCH 25/37] The browserMock $window must be reachable from the pushStateMock --- test-utils/browserMock.js | 6 +- test-utils/pushStateMock.js | 233 ++++++++++++++++++------------------ 2 files changed, 120 insertions(+), 119 deletions(-) diff --git a/test-utils/browserMock.js b/test-utils/browserMock.js index d667772a..ead6e9e4 100644 --- a/test-utils/browserMock.js +++ b/test-utils/browserMock.js @@ -5,14 +5,14 @@ var domMock = require("./domMock") var xhrMock = require("./xhrMock") module.exports = function(env) { - var $window = {} + env = env || {} + var $window = env.window = {} var dom = domMock() var xhr = xhrMock() - var ps = pushStateMock(env) for (var key in dom) if (!$window[key]) $window[key] = dom[key] for (var key in xhr) if (!$window[key]) $window[key] = xhr[key] - for (var key in ps) if (!$window[key]) $window[key] = ps[key] + pushStateMock(env) return $window } \ No newline at end of file diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index 3e4ed1f7..d23b3a1f 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -5,6 +5,7 @@ var parseURL = require("../test-utils/parseURL") module.exports = function(options) { if (options == null) options = {} + var $window = options.window || {} var protocol = options.protocol || "http:" var hostname = options.hostname || "localhost" var port = "" @@ -32,7 +33,7 @@ module.exports = function(options) { } return isNew } - + function prefix(prefix, value) { if (value === "") return "" return (value.charAt(0) !== prefix ? prefix : "") + value @@ -46,125 +47,125 @@ module.exports = function(options) { function unload() { if (typeof $window.onunload === "function") $window.onunload({type: "unload"}) } - var $window = { - location: { - get protocol() { - return protocol - }, - get hostname() { - return hostname - }, - get port() { - return port - }, - get pathname() { - return pathname - }, - get search() { - return search - }, - get hash() { - return hash - }, - get origin() { - if (protocol === "file:") return "null" - return protocol + "//" + hostname + prefix(":", port) - }, - get host() { - if (protocol === "file:") return "" - return hostname + prefix(":", port) - }, - get href() { - return getURL() - }, - set protocol(value) { - throw new Error("Protocol is read-only") - }, - set hostname(value) { - unload() - past.push({url: getURL(), isNew: true}) - future = [] - hostname = value - }, - set port(value) { - if (protocol === "file:") throw new Error("Port is read-only under `file://` protocol") - unload() - past.push({url: getURL(), isNew: true}) - future = [] - port = value - }, - set pathname(value) { - if (protocol === "file:") throw new Error("Pathname is read-only under `file://` protocol") - unload() - past.push({url: getURL(), isNew: true}) - future = [] - pathname = prefix("/", value) - }, - set search(value) { - unload() - past.push({url: getURL(), isNew: true}) - future = [] - search = prefix("?", value) - }, - set hash(value) { - var oldHash = hash - past.push({url: getURL(), isNew: false}) - future = [] - hash = prefix("#", value) - if (oldHash != hash) hashchange() - }, + $window.location = { + get protocol() { + return protocol + }, + get hostname() { + return hostname + }, + get port() { + return port + }, + get pathname() { + return pathname + }, + get search() { + return search + }, + get hash() { + return hash + }, + get origin() { + if (protocol === "file:") return "null" + return protocol + "//" + hostname + prefix(":", port) + }, + get host() { + if (protocol === "file:") return "" + return hostname + prefix(":", port) + }, + get href() { + return getURL() + }, - set origin(value) { - //origin is writable but ignored - }, - set host(value) { - //host is writable but ignored in Chrome - }, - set href(value) { - var url = getURL() - var isNew = setURL(value) - if (isNew) { - setURL(url) - unload() - setURL(value) - } - past.push({url: url, isNew: isNew}) - future = [] - }, + set protocol(value) { + throw new Error("Protocol is read-only") }, - history: { - pushState: function(data, title, url) { - past.push({url: getURL(), isNew: false}) - future = [] - setURL(url) - }, - replaceState: function(data, title, url) { - future = [] - setURL(url) - }, - back: function() { - var entry = past.pop() - if (entry != null) { - if (entry.isNew) unload() - future.push({url: getURL(), isNew: false}) - setURL(entry.url) - if (!entry.isNew) popstate() - } - }, - forward: function() { - var entry = future.pop() - if (entry != null) { - if (entry.isNew) unload() - past.push({url: getURL(), isNew: false}) - setURL(entry.url) - if (!entry.isNew) popstate() - } - }, + set hostname(value) { + unload() + past.push({url: getURL(), isNew: true}) + future = [] + hostname = value + }, + set port(value) { + if (protocol === "file:") throw new Error("Port is read-only under `file://` protocol") + unload() + past.push({url: getURL(), isNew: true}) + future = [] + port = value + }, + set pathname(value) { + if (protocol === "file:") throw new Error("Pathname is read-only under `file://` protocol") + unload() + past.push({url: getURL(), isNew: true}) + future = [] + pathname = prefix("/", value) + }, + set search(value) { + unload() + past.push({url: getURL(), isNew: true}) + future = [] + search = prefix("?", value) + }, + set hash(value) { + var oldHash = hash + past.push({url: getURL(), isNew: false}) + future = [] + hash = prefix("#", value) + if (oldHash != hash) hashchange() + }, + + set origin(value) { + //origin is writable but ignored + }, + set host(value) { + //host is writable but ignored in Chrome + }, + set href(value) { + var url = getURL() + var isNew = setURL(value) + if (isNew) { + setURL(url) + unload() + setURL(value) + } + past.push({url: url, isNew: isNew}) + future = [] }, - onpopstate: null, - onhashchange: null, - onunload: null, } + $window.history = { + pushState: function(data, title, url) { + past.push({url: getURL(), isNew: false}) + future = [] + setURL(url) + }, + replaceState: function(data, title, url) { + future = [] + setURL(url) + }, + back: function() { + var entry = past.pop() + if (entry != null) { + if (entry.isNew) unload() + future.push({url: getURL(), isNew: false}) + setURL(entry.url) + if (!entry.isNew) popstate() + } + }, + forward: function() { + var entry = future.pop() + if (entry != null) { + if (entry.isNew) unload() + past.push({url: getURL(), isNew: false}) + setURL(entry.url) + if (!entry.isNew) popstate() + } + }, + } + $window.onpopstate = null, + $window.onhashchange = null, + $window.onunload = null + return $window } From 3a671fc5cad4d485098fffd82ef1f8fa85db73f1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Wed, 7 Dec 2016 15:03:49 +0100 Subject: [PATCH 26/37] [router] add tests for onmatch corner cases --- api/tests/test-router.js | 128 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 8fc6c3bc..7ea00195 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -627,7 +627,7 @@ o.spec("route", function() { render: render }, "/b" : { - view: function(vnode){ + view: function() { redirected = true } } @@ -644,6 +644,7 @@ o.spec("route", function() { o("onmatch can redirect to another route that has RouteResolver w/ only onmatch", function(done) { var redirected = false var render = o.spy() + var view = o.spy(function() {return m("div")}) $window.location.href = prefix + "/a" route(root, "/a", { @@ -656,16 +657,21 @@ o.spec("route", function() { "/b" : { onmatch: function() { redirected = true - return {view: function() {}} + return {view: view} } } }) callAsync(function() { - o(render.callCount).equals(0) - o(redirected).equals(true) + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + o(view.callCount).equals(1) + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") - done() + done() + }) }) }) @@ -696,6 +702,118 @@ o.spec("route", function() { }) }) + o("onmatch can redirect to another route that has RouteResolver whose onmatch resolves asynchronously", function(done) { + var redirected = false + var render = o.spy() + var view = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + route.set("/b") + }, + render: render + }, + "/b" : { + onmatch: function() { + redirected = true + return new Promise(function(fulfill){ + callAsync(function(){ + fulfill({view: view}) + }) + }) + } + } + }) + + callAsync(function() { + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + o(view.callCount).equals(1) + + done() + }) + }) + }) + }) + + o("onmatch can redirect to another route asynchronously", function(done) { + var redirected = false + var render = o.spy() + var view = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + callAsync(function() {route.set("/b")}) + return new Promise(function() {}) + }, + render: render + }, + "/b" : { + onmatch: function() { + redirected = true + return {view: view} + } + } + }) + + callAsync(function() { + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(redirected).equals(true) + o(view.callCount).equals(1) + + done() + }) + }) + }) + }) + + o("onmatch can redirect w/ window.history.back()", function(done) { + + var render = o.spy() + var component = {view: o.spy()} + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + return component + }, + render: function(vnode) { + return vnode + } + }, + "/b" : { + onmatch: function() { + $window.history.back() + return new Promise(function() {}) + }, + render: render + } + }) + + callAsync(function() { + route.set('/b') + callAsync(function() { + callAsync(function() { + callAsync(function() { + o(render.callCount).equals(0) + o(component.view.callCount).equals(2) + + done() + }) + }) + }) + }) + }) + o("onmatch can redirect to a non-existent route that defaults to a RouteResolver w/ onmatch", function(done) { var redirected = false var render = o.spy() From d74fa7583245da85926d8bc8db4bf25254da76f9 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Wed, 7 Dec 2016 16:14:08 +0000 Subject: [PATCH 27/37] Test branched rejection handler resolution --- promise/tests/test-promise.js | 18 ++++++++++++++++++ request/tests/test-request.js | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js index 55b6e314..802c9f93 100644 --- a/promise/tests/test-promise.js +++ b/promise/tests/test-promise.js @@ -403,6 +403,24 @@ o.spec("promise", function() { done() }) }) + o("triggers all branched rejection handlers upon rejection", function(done) { + var promise = Promise.reject() + var then = o.spy() + var catch1 = o.spy() + var catch2 = o.spy() + var catch3 = o.spy() + + promise.catch(catch1) + promise.then(then, catch2).catch(catch3) + + promise.then(null, function(){ + o(catch1.callCount).equals(1, "first branch catch triggers") + o(then.callCount).equals(0, "second branch then resolution handler doesn't trigger") + o(catch2.callCount).equals(1, "second branch then rejection handler triggers") + o(catch3.callCount).equals(1, "second branch subseqent catch triggers") + done() + }) + }) o("does not absorb resolved promise via static rejector", function(done) { var promise = Promise.reject(Promise.resolve(1)) diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 1de21861..652f63bc 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -336,7 +336,7 @@ o.spec("xhr", function() { } }) var promise = xhr("/item", {background: true}).then(function() {}) - + setTimeout(function() { o(complete.callCount).equals(0) done() @@ -377,5 +377,28 @@ o.spec("xhr", function() { o(e.message).equals("error") }).then(done) }) + o("triggers all branched catches upon rejection", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 500, responseText: "error"} + } + }) + var request = xhr({method: "GET", url: "/item"}) + var then = o.spy() + var catch1 = o.spy() + var catch2 = o.spy() + var catch3 = o.spy() + + request.catch(catch1) + request.then(then, catch2).catch(catch3) + + setTimeout(function(){ + o(catch1.callCount).equals(1, "first branch catch triggers") + o(then.callCount).equals(0, "second branch then resolution handler doesn't trigger") + o(catch2.callCount).equals(1, "second branch then rejection handler triggers") + o(catch3.callCount).equals(1, "second branch subseqent catch triggers") + done() + }, 10) + }) }) }) From 04b860cefcd61eb58ceb206db7ed810eb1575673 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 11:18:54 -0500 Subject: [PATCH 28/37] refer to relevant section from onmatch docs --- docs/route.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/route.md b/docs/route.md index dcdddfa3..264d437e 100644 --- a/docs/route.md +++ b/docs/route.md @@ -16,10 +16,10 @@ - [Routing parameters](#routing-parameters) - [Changing router prefix](#changing-router-prefix) - [Advanced component resolution](#advanced-component-resolution) -- [Wrapping a layout component](#wrapping-a-layout-component) -- [Authentication](#authentication) -- [Preloading data](#preloading-data) -- [Code splitting](#code-splitting) + - [Wrapping a layout component](#wrapping-a-layout-component) + - [Authentication](#authentication) + - [Preloading data](#preloading-data) + - [Code splitting](#code-splitting) --- @@ -112,6 +112,8 @@ The `onmatch` hook is called when the router needs to find a component to render 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](#advanced-component-resolution) section + `routeResolver.onmatch(args, requestedPath)` Argument | Type | Description @@ -304,7 +306,7 @@ RouteResolvers are useful for implementing a variety of advanced routing use cas --- -### Wrapping a layout component +#### 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: @@ -348,7 +350,7 @@ Note that in this case, if the Layout component the `oninit` and `oncreate` life --- -### Authentication +#### Authentication The RouterResolver's `onmatch` hook can be used to run logic before the top level component in a route is initializated. The example below shows how to implement a login wall that prevents users from seeing the `/secret` page unless they login. @@ -387,7 +389,7 @@ For the sake of simplicity, in the example above, the user's logged in status is --- -### Preloading data +#### Preloading data Typically, a component can load data upon initialization. Loading data this way renders the component twice (once upon routing, and once after the request completes). @@ -443,7 +445,7 @@ Above, `render` only runs after the request completes, making the ternary operat --- -### Code splitting +#### 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: From 65ef101aff3da71053ae296933e86467c0704452 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 11:22:59 -0500 Subject: [PATCH 29/37] don't log intermediate uncaught promise errors in request --- README.md | 2 +- mithril.js | 2 +- mithril.min.js | 4 ++-- request/request.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b900337..3b577d13 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.49 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.50 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index b190fda0..02b87bb3 100644 --- a/mithril.js +++ b/mithril.js @@ -214,7 +214,7 @@ var _8 = function($window, Promise) { var next = then0.apply(promise0, arguments) next.then(complete, function(e) { complete() - throw e + if (count === 0) throw e }) return finalize(next) } diff --git a/mithril.min.js b/mithril.min.js index ea220c93..801c1241 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -6,8 +6,8 @@ void 0)};B.fragment=function(a,c){return y("[",a.key,a,y.normalizeChildren(c),vo h(k.bind(c))}}catch(G){n(G)}}}function h(a){function b(b){return function(a){0a.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function n(a){return a.responseText}function m(a, +"["+g+"]",d[g]);else if("[object Object]"===Object.prototype.toString.call(d))for(g in d)c(a+"["+g+"]",d[g]);else h.push(encodeURIComponent(a)+(null!=d&&""!==d?"="+encodeURIComponent(d):""))}if("[object Object]"!==Object.prototype.toString.call(a))return"";var h=[],d;for(d in a)c(d,a[d]);return h.join("&")},M=function(a,c){function h(){function b(){0===--a&&"function"===typeof t&&t()}var a=0;return function w(c){var d=c.then;c.then=function(){a++;var g=d.apply(c,arguments);g.then(b,function(c){b(); +if(0===a)throw c;});return w(g)};return c}}function d(b,a){if("string"===typeof b){var c=b;b=a||{};null==b.url&&(b.url=c)}return b}function g(b,a){if(null==a)return b;for(var c=b.match(/:[^\/]+/gi)||[],d=0;da.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function n(a){return a.responseText}function m(a, c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bz.status||304===z.status)c(m(b.type,a));else{var g=Error(z.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?z.send(b.data):z.send()});return!0===b.background?w:t(w)},jsonp:function(b,k){var n=h();b=d(b,k);var w=new c(function(c, diff --git a/request/request.js b/request/request.js index e813446b..2a9597ba 100644 --- a/request/request.js +++ b/request/request.js @@ -18,7 +18,7 @@ module.exports = function($window, Promise) { var next = then.apply(promise, arguments) next.then(complete, function(e) { complete() - throw e + if (count === 0) throw e }) return finalize(next) } From 0b84a3f39f67e170fe676819879fd01ff829fe6c Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 11:58:30 -0500 Subject: [PATCH 30/37] fix tests from #1470 --- promise/tests/test-promise.js | 25 ++++++++++++++----------- request/tests/index.html | 1 + request/tests/test-request.js | 28 ++++++++++++++++------------ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js index 802c9f93..aa3a1bd9 100644 --- a/promise/tests/test-promise.js +++ b/promise/tests/test-promise.js @@ -405,20 +405,23 @@ o.spec("promise", function() { }) o("triggers all branched rejection handlers upon rejection", function(done) { var promise = Promise.reject() - var then = o.spy() - var catch1 = o.spy() - var catch2 = o.spy() - var catch3 = o.spy() + var then = o.spy() + var catch1 = o.spy() + var catch2 = o.spy() + var catch3 = o.spy() promise.catch(catch1) - promise.then(then, catch2).catch(catch3) + promise.then(then, catch2) + promise.then(then).catch(catch3) - promise.then(null, function(){ - o(catch1.callCount).equals(1, "first branch catch triggers") - o(then.callCount).equals(0, "second branch then resolution handler doesn't trigger") - o(catch2.callCount).equals(1, "second branch then rejection handler triggers") - o(catch3.callCount).equals(1, "second branch subseqent catch triggers") - done() + callAsync(function() { + callAsync(function() { + o(catch1.callCount).equals(1) + o(then.callCount).equals(0) + o(catch2.callCount).equals(1) + o(catch3.callCount).equals(1) + done() + }) }) }) o("does not absorb resolved promise via static rejector", function(done) { diff --git a/request/tests/index.html b/request/tests/index.html index e30c178e..179c66a1 100644 --- a/request/tests/index.html +++ b/request/tests/index.html @@ -6,6 +6,7 @@ + diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 652f63bc..86643e95 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") var xhrMock = require("../../test-utils/xhrMock") var Request = require("../../request/request") var Promise = require("../../promise/promise") @@ -384,21 +385,24 @@ o.spec("xhr", function() { } }) var request = xhr({method: "GET", url: "/item"}) - var then = o.spy() - var catch1 = o.spy() - var catch2 = o.spy() - var catch3 = o.spy() + var then = o.spy() + var catch1 = o.spy() + var catch2 = o.spy() + var catch3 = o.spy() request.catch(catch1) - request.then(then, catch2).catch(catch3) + request.then(then, catch2) + request.then(then).catch(catch3) - setTimeout(function(){ - o(catch1.callCount).equals(1, "first branch catch triggers") - o(then.callCount).equals(0, "second branch then resolution handler doesn't trigger") - o(catch2.callCount).equals(1, "second branch then rejection handler triggers") - o(catch3.callCount).equals(1, "second branch subseqent catch triggers") - done() - }, 10) + callAsync(function() { + callAsync(function() { + o(catch1.callCount).equals(1) + o(then.callCount).equals(0) + o(catch2.callCount).equals(1) + o(catch3.callCount).equals(1) + done() + }) + }) }) }) }) From 00553b3ec8cd297c5edf2f4ba2ff80e7f8ca3b97 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Wed, 7 Dec 2016 09:35:34 -0800 Subject: [PATCH 31/37] Clean up npm package some (#1449) --- .npmignore | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..48873c87 --- /dev/null +++ b/.npmignore @@ -0,0 +1,20 @@ +# Configuration files +.deploy.env +.editorconfig +.eslintrc.js +.gitattributes +.gitignore +.travis.yml + +# Tests +test-utils/ +tests/ + +# Documentation +docs/ +examples/ +CONTRIBUTING.md + +# Browser stub (use index.js w/ a bundler or mithril.js w/o one instead) +module/ +browser.js From b1330667e40aec1894881a2bb6a7f16691bb056c Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 12:42:30 -0500 Subject: [PATCH 32/37] fix global assignment --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index eb2a7b31..cf25070e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -126,7 +126,7 @@ var querystring = m.buildQueryString({a: "1", b: "2"}) ```javascript var state = { value: "", - setValue: function(v) {value = v} + setValue: function(v) {state.value = v} } var Component = { From b9a479d45eba538d9c6a15397906cf5386385d4f Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 7 Dec 2016 12:45:42 -0500 Subject: [PATCH 33/37] lint docs --- docs/route.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/route.md b/docs/route.md index 264d437e..2d4112cc 100644 --- a/docs/route.md +++ b/docs/route.md @@ -405,7 +405,7 @@ var state = { m.route(document.body, "/user/list", { "/user/list": { - oninit: loadUsers, + oninit: state.loadUsers, view: function() { return state.users.length > 0 ? state.users.map(function() { return m("div", user.id) From fe2489df58031885ed9bf701b2cc0a424d4fe25b Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Wed, 7 Dec 2016 10:07:01 -0800 Subject: [PATCH 34/37] Data -> state --- docs/withAttr.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/withAttr.md b/docs/withAttr.md index ac000a22..4a9fd44b 100644 --- a/docs/withAttr.md +++ b/docs/withAttr.md @@ -63,18 +63,18 @@ document.body.onclick = m.withAttr("title", function(value) { Typically, `m.withAttr()` can be used in Mithril component views to avoid polluting the data layer with DOM event model concerns: ```javascript -var Data = { +var state = { email: "", setEmail: function(email) { - Data.email = email.toLowerCase() + state.email = email.toLowerCase() } } var MyComponent = { view: function() { return m("input", { - oninput: m.withAttr("value", Data.setEmail), - value: Data.email + oninput: m.withAttr("value", state.setEmail), + value: state.email }) } } @@ -89,15 +89,15 @@ m.mount(document.body, MyComponent) The `m.withAttr()` helper reads the value of the element to which the event handler is bound, which is not necessarily the same as the element where the event originated. ```javascript -var Data = { +var state = { url: "", - setURL: function(url) {Data.url = url} + setURL: function(url) {state.url = url} } var MyComponent = { view: function() { - return m("a[href='/foo']", {onclick: m.withAttr("href", Data.setURL)}, [ - m("span", Data.url) + return m("a[href='/foo']", {onclick: m.withAttr("href", state.setURL)}, [ + m("span", state.url) ]) } } @@ -117,21 +117,21 @@ The first argument of `m.withAttr()` can be either an attribute or a property. ```javascript // reads from `select.selectedIndex` property -var Data = { +var state = { index: 0, - setIndex: function(index) {Data.index = index} + setIndex: function(index) {state.index = index} } -m("select", {onclick: m.withAttr("selectedIndex", Data.setIndex)}) +m("select", {onclick: m.withAttr("selectedIndex", state.setIndex)}) ``` If a value can be both an attribute *and* a property, the property value is used. ```javascript // value is a boolean, because the `input.checked` property is boolean -var Data = { +var state = { selected: false, - setSelected: function(selected) {Data.selected = selected} + setSelected: function(selected) {state.selected = selected} } -m("input[type=checkbox]", {onclick: m.withAttr("checked", Data.setSelected)}) +m("input[type=checkbox]", {onclick: m.withAttr("checked", state.setSelected)}) ``` From f8bb81f539e8f83b856e1f28353f8e80d4e39650 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Wed, 7 Dec 2016 23:03:39 +0100 Subject: [PATCH 35/37] [api/router] simplify the route finalization logic --- api/router.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/api/router.js b/api/router.js index ade067ec..1e1650d5 100644 --- a/api/router.js +++ b/api/router.js @@ -8,11 +8,11 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var routing = false, render, component, attrs, currentPath, resolve + var render, component, attrs, currentPath, updatePending = false var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, resolve = null + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, updatePending = false render = (routeResolver.render || identity).bind(routeResolver) run() } @@ -26,15 +26,10 @@ module.exports = function($window, redrawService) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (resolve != null) update(payload, component, params, path) - else { - resolve = function(resolved) { - update(payload, resolved, params, path) - } - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (resolve != null) resolve(resolved) - }, bail) - } + updatePending = true + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + if (updatePending) update(payload, resolved, params, path) + }, bail) } else update(payload, "div", params, path) } @@ -42,8 +37,8 @@ module.exports = function($window, redrawService) { redrawService.subscribe(root, run) } route.set = function(path, data, options) { - if (resolve != null) options = {replace: true} - resolve = null + if (updatePending) options = {replace: true} + updatePending = false routeService.setPath(path, data, options) } route.get = function() {return currentPath} From 48d786b08553d5463d8ed14e6fb6c95610801520 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 7 Dec 2016 22:10:29 +0000 Subject: [PATCH 36/37] Bundled output for commit df7d2c51ccb42de8b061e5f032ffe138c14e257f [skip ci] --- README.md | 2 +- mithril.js | 21 +++++-------- mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 3 files changed, 50 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 3b577d13..11a87ed7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.50 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.47 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 02b87bb3..e5558df4 100644 --- a/mithril.js +++ b/mithril.js @@ -1081,11 +1081,11 @@ var coreRouter = function($window) { var _20 = function($window, redrawService0) { var routeService = coreRouter($window) var identity = function(v) {return v} - var routing = false, render1, component, attrs3, currentPath, resolve + var render1, component, attrs3, currentPath, updatePending = false var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") var update = function(routeResolver, comp, params, path) { - component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, resolve = null + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, updatePending = false render1 = (routeResolver.render || identity).bind(routeResolver) run1() } @@ -1099,15 +1099,10 @@ var _20 = function($window, redrawService0) { if (payload.view) update({}, payload, params, path) else { if (payload.onmatch) { - if (resolve != null) update(payload, component, params, path) - else { - resolve = function(resolved) { - update(payload, resolved, params, path) - } - Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (resolve != null) resolve(resolved) - }, bail) - } + updatePending = true + Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { + if (updatePending) update(payload, resolved, params, path) + }, bail) } else update(payload, "div", params, path) } @@ -1115,8 +1110,8 @@ var _20 = function($window, redrawService0) { redrawService0.subscribe(root, run1) } route.set = function(path, data, options) { - if (resolve != null) options = {replace: true} - resolve = null + if (updatePending) options = {replace: true} + updatePending = false routeService.setPath(path, data, options) } route.get = function() {return currentPath} diff --git a/mithril.min.js b/mithril.min.js index 801c1241..69e76332 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function y(a,c,h,d,g,l){return{tag:a,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function B(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===K[a]){for(var c,h,d=[],g={};c=P.exec(a);){var l=c[1],k=c[2];""===l&&""!==k?h=k:"#"===l?g.id=k:"."===l?d.push(k):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function n(a){return a.responseText}function m(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bz.status||304===z.status)c(m(b.type,a));else{var g=Error(z.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?z.send(b.data):z.send()});return!0===b.background?w:t(w)},jsonp:function(b,k){var n=h();b=d(b,k);var w=new c(function(c, -d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+p++,k=a.document.createElement("script");a[h]=function(d){k.parentNode.removeChild(k);c(m(b.type,d));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?w:n(w)},setCompletionCallback:function(a){t=a}}}(window, -H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=u&&w>=A;){var r=f[u],v=a[A];if(r!==v||q)if(null==r)u++;else if(null==v)A++;else if(r.key===v.key)u++,A++,l(e,r,v,b,n(f,u,d),q,g),q&&r.tag===v.tag&&m(e,k(r),d);else if(r=f[p],r!==v||q)if(null==r)p--;else if(null==v)A++;else if(r.key===v.key)l(e,r,v,b,n(f,p+1,d),q,g),(q||A=u&&w>=A;){r=f[p];v=a[w];if(r!==v||q)if(null==r)p--;else{if(null!= -v)if(r.key===v.key)l(e,r,v,b,n(f,p+1,d),q,g),q&&r.tag===v.tag&&m(e,k(r),d),null!=r.dom&&(d=r.dom),p--;else{if(!z){z=f;var r=p,D={},y;for(y=0;ya.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c, +d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window, +H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&t>=A;){var x=f[k],u=a[A];if(x!==u||q)if(null==x)k++;else if(null==u)A++;else if(x.key===u.key)k++,A++,m(e,x,u,b,n(f,k,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d);else if(x=f[y],x!==u||q)if(null==x)y--;else if(null==u)A++;else if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),(q||A=k&&t>=A;){x=f[y];u=a[t];if(x!==u||q)if(null==x)y--;else{if(null!= +u)if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d),null!=x.dom&&(d=x.dom),y--;else{if(!D){D=f;var x=y,C={},w;for(w=0;w Date: Thu, 15 Dec 2016 07:04:33 +0000 Subject: [PATCH 37/37] Fix anchor --- docs/mount.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mount.md b/docs/mount.md index a188668b..f8a28e95 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -4,7 +4,7 @@ - [Signature](#signature) - [How it works](#how-it-works) - [Performance considerations](#performance-considerations) -- [Differences from m.render](#differences-from-m-render) +- [Differences from m.render](#differences-from-mrender) --- @@ -73,4 +73,4 @@ A component rendered via `m.mount` automatically auto-redraws in response to vie `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). \ No newline at end of file +`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).