From 50b8b498c5807e2f219880cadf03405ce36cc56e Mon Sep 17 00:00:00 2001 From: Leo Date: Wed, 7 Dec 2016 18:39:21 -0500 Subject: [PATCH 01/54] bump ospec version --- ospec/README.md | 2 +- ospec/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ospec/README.md b/ospec/README.md index 0d116ec6..cd08c697 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -4,7 +4,7 @@ Noiseless testing framework -Version: 1.2 +Version: 1.2.1 License: MIT ## About diff --git a/ospec/package.json b/ospec/package.json index 78fffa34..940b7f5a 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -1,6 +1,6 @@ { "name": "ospec", - "version": "1.2.0", + "version": "1.2.1", "description": "Noiseless testing framework", "main": "ospec.js", "directories": { From 605fa447111711b28aba24570d34f7f5a0230b05 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Thu, 8 Dec 2016 15:18:37 +0100 Subject: [PATCH 02/54] [api/router] Test for two deferred onmatch racing --- api/tests/test-router.js | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 7ea00195..7424e929 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -932,6 +932,63 @@ o.spec("route", function() { }) }) + o.only("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { + var renderA = o.spy() + var renderB = o.spy() + var onmatchA = o.spy(function(){ + return new Promise(function(fulfill) { + setTimeout(function(){ + fulfill() + }, 10) + }) + }) + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a": { + onmatch: onmatchA, + render: renderA + }, + "/b": { + onmatch: function(){ + var p = new Promise(function(fulfill) { + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + + setTimeout(function(){ + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + + fulfill() + + p.then(function(){ + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(1) + + done() + }) + }, 20) + }) + return p + }, + render: renderB + } + }) + + callAsync(function() { + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + route.set("/b") + o(onmatchA.callCount).equals(1) + o(renderA.callCount).equals(0) + o(renderB.callCount).equals(0) + }) + }) + 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")}) From 703aab7905c6c066b427307dbad3f27247987d07 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Thu, 8 Dec 2016 21:44:21 +0100 Subject: [PATCH 03/54] [router] Fix hopefully the last race condition --- api/router.js | 24 ++++++++++++------------ api/tests/test-router.js | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/router.js b/api/router.js index 1e1650d5..ccfccb94 100644 --- a/api/router.js +++ b/api/router.js @@ -8,14 +8,9 @@ module.exports = function($window, redrawService) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render, component, attrs, currentPath, updatePending = false + var render, component, attrs, currentPath, lastUpdate 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, updatePending = false - render = (routeResolver.render || identity).bind(routeResolver) - run() - } var run = function() { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } @@ -23,22 +18,27 @@ module.exports = function($window, redrawService) { routeService.setPath(defaultRoute) } routeService.defineRoutes(routes, function(payload, params, path) { - if (payload.view) update({}, payload, params, path) + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs = params, currentPath = path, lastUpdate = null + render = (routeResolver.render || identity).bind(routeResolver) + run() + } + if (payload.view) update({}, payload) else { if (payload.onmatch) { - updatePending = true Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (updatePending) update(payload, resolved, params, path) + update(payload, resolved) }, bail) } - else update(payload, "div", params, path) + else update(payload, "div") } }, bail) redrawService.subscribe(root, run) } route.set = function(path, data, options) { - if (updatePending) options = {replace: true} - updatePending = false + if (lastUpdate != null) options = {replace: true} + lastUpdate = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 7424e929..7e2454cb 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -932,7 +932,7 @@ o.spec("route", function() { }) }) - o.only("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { + o("when two async routes are racing, the last one set cancels the finalization of the first", function(done) { var renderA = o.spy() var renderB = o.spy() var onmatchA = o.spy(function(){ From 45c6b473b8be5993d8c1f9b5b754261d44f04e8d Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 16 Dec 2016 10:34:12 +0000 Subject: [PATCH 04/54] Revert #1449 --- .npmignore | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.npmignore b/.npmignore index 48873c87..bf023d92 100644 --- a/.npmignore +++ b/.npmignore @@ -1,20 +1,8 @@ -# Configuration files +# Development-specific 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 c95629c62352c453920a71a9c5bcca1e9a186ed8 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 16 Dec 2016 15:21:57 -0500 Subject: [PATCH 05/54] handles noop from undefined to null #1473 --- render/render.js | 2 +- render/tests/test-updateNodes.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/render/render.js b/render/render.js index 9ee2628e..023491f3 100644 --- a/render/render.js +++ b/render/render.js @@ -134,7 +134,7 @@ module.exports = function($window) { if (old.length === vnodes.length && isUnkeyed) { for (var i = 0; i < old.length; i++) { if (old[i] === vnodes[i]) continue - else if (old[i] == null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) + else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) } diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 7b0f9715..524db029 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -104,6 +104,15 @@ o.spec("updateNodes", function() { o(updated[0].dom.nodeValue).equals("a") o(updated[0].dom).equals(root.childNodes[0]) }) + o("handles undefined to null noop", function() { + var vnodes = [null, {tag: "div"}] + var updated = [undefined, {tag: "div"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + }) o("reverses els w/ even count", function() { var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] var updated = [{tag: "s", key: 4}, {tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}] From 2b90b37e72270d306a4e7778f5c403214c1f83f0 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Fri, 16 Dec 2016 20:23:33 +0000 Subject: [PATCH 06/54] Bundled output for commit 297146a2cc041d1b41b8255ba35efc1358ec80eb [skip ci] --- mithril.js | 2 +- mithril.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mithril.js b/mithril.js index e5558df4..7ca741d0 100644 --- a/mithril.js +++ b/mithril.js @@ -463,7 +463,7 @@ var coreRenderer = function($window) { if (old.length === vnodes.length && isUnkeyed) { for (var i = 0; i < old.length; i++) { if (old[i] === vnodes[i]) continue - else if (old[i] == null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) + else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) } diff --git a/mithril.min.js b/mithril.min.js index 69e76332..d4facde4 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -16,7 +16,7 @@ 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: Fri, 16 Dec 2016 15:28:07 -0500 Subject: [PATCH 07/54] make note in docs about m.route singletonness --- docs/route.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/route.md b/docs/route.md index 2d4112cc..f941bce7 100644 --- a/docs/route.md +++ b/docs/route.md @@ -39,6 +39,8 @@ m.route(document.body, "/home", { }) ``` +You can only have one `m.route` call per application. + --- ### Signature From 0c3c6a5af968f635a94f8eef04eed983f0924ab9 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sat, 17 Dec 2016 22:57:46 +0000 Subject: [PATCH 08/54] Bundled output for commit fe8a37dcb7fae45732ffff4c26fc87b8f5bda62d [skip ci] --- mithril.js | 24 +++++++++---------- mithril.min.js | 64 +++++++++++++++++++++++++------------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/mithril.js b/mithril.js index 7ca741d0..2e3af836 100644 --- a/mithril.js +++ b/mithril.js @@ -1081,14 +1081,9 @@ var coreRouter = function($window) { var _20 = function($window, redrawService0) { var routeService = coreRouter($window) var identity = function(v) {return v} - var render1, component, attrs3, currentPath, updatePending = false + var render1, component, attrs3, currentPath, lastUpdate 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, updatePending = false - render1 = (routeResolver.render || identity).bind(routeResolver) - run1() - } var run1 = function() { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } @@ -1096,22 +1091,27 @@ var _20 = function($window, redrawService0) { routeService.setPath(defaultRoute) } routeService.defineRoutes(routes, function(payload, params, path) { - if (payload.view) update({}, payload, params, path) + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, lastUpdate = null + render1 = (routeResolver.render || identity).bind(routeResolver) + run1() + } + if (payload.view) update({}, payload) else { if (payload.onmatch) { - updatePending = true Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { - if (updatePending) update(payload, resolved, params, path) + update(payload, resolved) }, bail) } - else update(payload, "div", params, path) + else update(payload, "div") } }, bail) redrawService0.subscribe(root, run1) } route.set = function(path, data, options) { - if (updatePending) options = {replace: true} - updatePending = false + if (lastUpdate != null) options = {replace: true} + lastUpdate = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} diff --git a/mithril.min.js b/mithril.min.js index d4facde4..0c599ac7 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function w(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, +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===J[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(C){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=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[y],w!==u||q)if(null==w)y--;else if(null==u)A++;else if(w.key===u.key)m(e,w,u,b,n(f,y+1,d),q,g),(q||A=k&&t>=A;){w=f[y];u=a[t];if(w!==u||q)if(null==w)y--;else{if(null!= +u)if(w.key===u.key)m(e,w,u,b,n(f,y+1,d),q,g),q&&w.tag===u.tag&&p(e,l(w),d),null!=w.dom&&(d=w.dom),y--;else{if(!C){C=f;var w=y,v={},D;for(D=0;D Date: Sat, 17 Dec 2016 21:50:04 -0500 Subject: [PATCH 09/54] add support for route state #1480 --- README.md | 2 +- docs/route.md | 69 +++++++++++- mithril.js | 147 +++++++++++++------------ mithril.min.js | 82 +++++++------- render/render.js | 139 +++++++++++------------ render/tests/test-updateNodes.js | 12 +- router/router.js | 10 +- router/tests/test-setPath.js | 12 ++ test-utils/pushStateMock.js | 25 +++-- test-utils/tests/test-pushStateMock.js | 65 +++++++++++ 10 files changed, 367 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index 11a87ed7..ac65b7f1 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.53 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/docs/route.md b/docs/route.md index f941bce7..5ce6d1cd 100644 --- a/docs/route.md +++ b/docs/route.md @@ -14,6 +14,9 @@ - [Typical usage](#typical-usage) - [Navigating to different routes](#navigating-to-different-routes) - [Routing parameters](#routing-parameters) + - [Key parameter](#key-parameter) + - [Variadic routes](#variadic-routes) + - [History state](#history-state) - [Changing router prefix](#changing-router-prefix) - [Advanced component resolution](#advanced-component-resolution) - [Wrapping a layout component](#wrapping-a-layout-component) @@ -70,6 +73,8 @@ Argument | Type | Required | Description `path` | `String` | Yes | The path to route to, without a prefix. The path may include slots for routing parameters `data` | `Object` | No | Routing parameters. If `path` has routing parameter slots, the properties of this object are interpolated into the path string `options.replace` | `Boolean` | No | Whether to create a new history entry or to replace the current one. Defaults to false +`options.state` | `Object` | No | The `state` object to pass to the underlying `history.pushState` / `history.replaceState` call. This state object becomes available in the `history.state` property, and is merged into the [routing parameters](#routing-parameters) object. Note that this option only works when using the pushState API, but is ignored if the router falls back to hashchange mode (i.e. if the pushState API is not available) +`options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call. **returns** | | | Returns `undefined` ##### route.get @@ -254,6 +259,28 @@ It's possible to have multiple arguments in a route, for example `/edit/:project In addition to routing parameters, the `attrs` object also includes a `path` property that contains the current route path, and a `route` property that contains the matched routed. +#### Key parameter + +When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from `/page/1` to `/page/2` given a route `/page/:id`, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the `onupdate` hook, rather than `oninit`/`oncreate`. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event. + +To achieve that, it's possible to combine route parameterization with the virtual dom [key reconciliation](keys.md) feature: + +```javascript +m.route(document.body, "/edit/1", { + "/edit/:key": Edit, +}) +``` + +This means that the [vnode](vnodes.md) that is created for the root component of the route has a route parameter object `key`. Route parameters become `attrs` in the vnode. Thus, when jumping from one page to another, the `key` changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities). + +You can take that idea further to create components that recreate themselves when reloaded: + +`m.route.set(m.route.get(), {key: Date.now()})` + +Or even use the [`history state`](#history-state) feature to achieve reloadable components without polluting the URL: + +`m.route.set(m.route.get(), null, {state: {key: Date.now()}})` + #### Variadic routes It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes: @@ -264,6 +291,44 @@ m.route(document.body, "/edit/pictures/image.jpg", { }) ``` +#### History state + +It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form. + +For example, you could create a form like this: + +```javascript +var state = { + term: "", + search: function() { + // save the state for this route + // this is equivalent to `history.replaceState({term: state.term}, null, location.href)` + m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}}) + + // navigate away + location.href = "https://google.com/?q=" + state.term + } +} + +var Form = { + oninit: function(vnode) { + state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button + }, + view: function() { + return m("form", [ + m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}), + m("button", {onclick: state.search}, "Search") + ]) + } +} + +m.route(document.body, "/", { + "/": Form, +}) +``` + +This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce. + --- ### Changing router prefix @@ -409,7 +474,7 @@ m.route(document.body, "/user/list", { "/user/list": { oninit: state.loadUsers, view: function() { - return state.users.length > 0 ? state.users.map(function() { + return state.users.length > 0 ? state.users.map(function(user) { return m("div", user.id) }) : "loading" } @@ -435,7 +500,7 @@ m.route(document.body, "/user/list", { "/user/list": { onmatch: state.loadUsers, render: function() { - return state.users.length > 0 ? state.users.map(function() { + return state.users.length > 0 ? state.users.map(function(user) { return m("div", user.id) }) : "loading" } diff --git a/mithril.js b/mithril.js index 7ca741d0..9ff9adfc 100644 --- a/mithril.js +++ b/mithril.js @@ -453,83 +453,84 @@ var coreRenderer = function($window) { else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) else { - var isUnkeyed = false - for (var i = 0; i < vnodes.length; i++) { - if (vnodes[i] != null) { - isUnkeyed = vnodes[i].key == null - break + if (old.length === vnodes.length) { + var isUnkeyed = false + for (var i = 0; i < vnodes.length; i++) { + if (vnodes[i] != null && old[i] != null) { + isUnkeyed = vnodes[i].key == null && old[i].key == null + break + } + } + if (isUnkeyed) { + for (var i = 0; i < old.length; i++) { + if (old[i] === vnodes[i]) continue + else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) + else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) + else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) + } + return } } - if (old.length === vnodes.length && isUnkeyed) { - for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) - else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) + var recycling = isRecyclable(old, vnodes) + if (recycling) old = old.concat(old.pool) + var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + while (oldEnd >= oldStart && end >= start) { + var o = old[oldStart], v = vnodes[start] + if (o === v && !recycling) oldStart++, start++ + else if (o == null) oldStart++ + else if (v == null) start++ + else if (o.key === v.key) { + oldStart++, start++ + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) + if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) } - } - else { - var recycling = isRecyclable(old, vnodes) - if (recycling) old = old.concat(old.pool) - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map - while (oldEnd >= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ - else if (o == null) oldStart++ + else { + var o = old[oldEnd] + if (o === v && !recycling) oldEnd--, start++ + else if (o == null) oldEnd-- else if (v == null) start++ - else if (o.key === v.key) { - oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ - else if (o == null) oldEnd-- - else if (v == null) start++ - else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) - oldEnd--, start++ - } - else break - } - } - while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- - else if (o == null) oldEnd-- - else if (v == null) end-- else if (o.key === v.key) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - if (o.dom != null) nextSibling = o.dom - oldEnd--, end-- + if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + oldEnd--, start++ } - else { - if (!map) map = getKeyMap(old, oldEnd) - if (v != null) { - var oldIndex = map[v.key] - if (oldIndex != null) { - var movable = old[oldIndex] - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom - } - else { - var dom = createNode(v, hooks, undefined) - insertNode(parent, dom, nextSibling) - nextSibling = dom - } - } - end-- - } - if (end < start) break + else break } - createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) } + while (oldEnd >= oldStart && end >= start) { + var o = old[oldEnd], v = vnodes[end] + if (o === v && !recycling) oldEnd--, end-- + else if (o == null) oldEnd-- + else if (v == null) end-- + else if (o.key === v.key) { + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + if (o.dom != null) nextSibling = o.dom + oldEnd--, end-- + } + else { + if (!map) map = getKeyMap(old, oldEnd) + if (v != null) { + var oldIndex = map[v.key] + if (oldIndex != null) { + var movable = old[oldIndex] + updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + insertNode(parent, toFragment(movable), nextSibling) + old[oldIndex].skip = true + if (movable.dom != null) nextSibling = movable.dom + } + else { + var dom = createNode(v, hooks, undefined) + insertNode(parent, dom, nextSibling) + nextSibling = dom + } + } + end-- + } + if (end < start) break + } + createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + removeNodes(old, oldStart, oldEnd + 1, vnodes) } } function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { @@ -1042,9 +1043,11 @@ var coreRouter = function($window) { var hash = buildQueryString(hashData) if (hash) path += "#" + hash if (supportsPushState) { - if (options && options.replace) $window.history.replaceState(null, null, router.prefix + path) - else $window.history.pushState(null, null, router.prefix + path) + var state = options ? options.state : null + var title = options ? options.title : null $window.onpopstate() + if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path) + else $window.history.pushState(state, title, router.prefix + path) } else $window.location.href = router.prefix + path } @@ -1054,6 +1057,10 @@ var coreRouter = function($window) { var params = {} var pathname = parsePath(path, params, params) + var state = $window.history.state + if (state != null) { + for (var k in state) params[k] = state[k] + } for (var route0 in routes) { var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") if (matcher.test(pathname)) { diff --git a/mithril.min.js b/mithril.min.js index d4facde4..524cb9d3 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function w(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(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;wa.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(C){throw Error(a);}}function m(a){return a.responseText}function q(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bp.status||304===p.status)c(q(b.type,a));else{var g=Error(p.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?p.send(b.data):p.send()});return!0===b.background?x:t(x)},jsonp:function(b,k){var m=h();b=d(b,k);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(q(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:m(t)},setCompletionCallback:function(a){t=a}}}(window, +H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=v&&x>=A;){var p=f[v],n=a[A];if(p!==n||u)if(null==p)v++;else if(null==n)A++;else if(p.key===n.key)v++,A++,l(e,p,n,b,m(f,v,d),u,g),u&&p.tag===n.tag&&q(e,k(p),d);else if(p=f[r],p!==n||u)if(null==p)r--;else if(null==n)A++;else if(p.key===n.key)l(e,p,n,b,m(f,r+1,d),u,g),(u||A=v&&x>=A;){p=f[r];n=a[x]; +if(p!==n||u)if(null==p)r--;else{if(null!=n)if(p.key===n.key)l(e,p,n,b,m(f,r+1,d),u,g),u&&p.tag===n.tag&&q(e,k(p),d),null!=p.dom&&(d=p.dom),r--;else{if(!C){C=f;var p=r,E={},w;for(w=0;w= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ - else if (o == null) oldStart++ + var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + while (oldEnd >= oldStart && end >= start) { + var o = old[oldStart], v = vnodes[start] + if (o === v && !recycling) oldStart++, start++ + else if (o == null) oldStart++ + else if (v == null) start++ + else if (o.key === v.key) { + oldStart++, start++ + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) + if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + } + else { + var o = old[oldEnd] + if (o === v && !recycling) oldEnd--, start++ + else if (o == null) oldEnd-- else if (v == null) start++ - else if (o.key === v.key) { - oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ - else if (o == null) oldEnd-- - else if (v == null) start++ - else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) - oldEnd--, start++ - } - else break - } - } - while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- - else if (o == null) oldEnd-- - else if (v == null) end-- else if (o.key === v.key) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - if (o.dom != null) nextSibling = o.dom - oldEnd--, end-- + if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + oldEnd--, start++ } - else { - if (!map) map = getKeyMap(old, oldEnd) - if (v != null) { - var oldIndex = map[v.key] - if (oldIndex != null) { - var movable = old[oldIndex] - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom - } - else { - var dom = createNode(v, hooks, undefined) - insertNode(parent, dom, nextSibling) - nextSibling = dom - } - } - end-- - } - if (end < start) break + else break } - createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) } + while (oldEnd >= oldStart && end >= start) { + var o = old[oldEnd], v = vnodes[end] + if (o === v && !recycling) oldEnd--, end-- + else if (o == null) oldEnd-- + else if (v == null) end-- + else if (o.key === v.key) { + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + if (o.dom != null) nextSibling = o.dom + oldEnd--, end-- + } + else { + if (!map) map = getKeyMap(old, oldEnd) + if (v != null) { + var oldIndex = map[v.key] + if (oldIndex != null) { + var movable = old[oldIndex] + updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + insertNode(parent, toFragment(movable), nextSibling) + old[oldIndex].skip = true + if (movable.dom != null) nextSibling = movable.dom + } + else { + var dom = createNode(v, hooks, undefined) + insertNode(parent, dom, nextSibling) + nextSibling = dom + } + } + end-- + } + if (end < start) break + } + createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) + removeNodes(old, oldStart, oldEnd + 1, vnodes) } } function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 524db029..b0e8c337 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -880,7 +880,7 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) o("cached, keyed nodes skip diff", function () { - var onupdate = o.spy(); + var onupdate = o.spy() var cached = {tag:"a", key:"a", attrs:{onupdate: onupdate}} render(root, cached) @@ -926,4 +926,14 @@ o.spec("updateNodes", function() { o(update.callCount).equals(2) o(remove.callCount).equals(0) }) + o("component is recreated if key changes to undefined", function () { + var vnode = {tag: "b", key: 1} + var updated = {tag: "b"} + + render(root, vnode) + var dom = vnode.dom + render(root, updated) + + o(vnode.dom).notEquals(updated.dom) + }) }) diff --git a/router/router.js b/router/router.js index 6e1d7270..51961e99 100644 --- a/router/router.js +++ b/router/router.js @@ -67,9 +67,11 @@ module.exports = function($window) { if (hash) path += "#" + hash if (supportsPushState) { - if (options && options.replace) $window.history.replaceState(null, null, router.prefix + path) - else $window.history.pushState(null, null, router.prefix + path) + var state = options ? options.state : null + var title = options ? options.title : null $window.onpopstate() + if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path) + else $window.history.pushState(state, title, router.prefix + path) } else $window.location.href = router.prefix + path } @@ -79,6 +81,10 @@ module.exports = function($window) { var params = {} var pathname = parsePath(path, params, params) + var state = $window.history.state + if (state != null) { + for (var k in state) params[k] = state[k] + } for (var route in routes) { var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") diff --git a/router/tests/test-setPath.js b/router/tests/test-setPath.js index c957a59a..d18c7525 100644 --- a/router/tests/test-setPath.js +++ b/router/tests/test-setPath.js @@ -150,6 +150,18 @@ o.spec("Router.setPath", function() { o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") + done() + }) + }) + o("state works", function(done) { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail) + + callAsync(function() { + router.setPath("/other", null, {state: {a: 1}}) + + o($window.history.state).deepEquals({a: 1}) + done() }) }) diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index d23b3a1f..91e7a136 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -13,7 +13,7 @@ module.exports = function(options) { var search = "" var hash = "" - var past = [], future = [] + var past = [{url: getURL(), isNew: true, state: null, title: null}], future = [] function getURL() { if (protocol === "file:") return protocol + "//" + pathname + search + hash @@ -42,7 +42,7 @@ module.exports = function(options) { if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) } function popstate() { - if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate"}) + if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate", state: $window.history.state}) } function unload() { if (typeof $window.onunload === "function") $window.onunload({type: "unload"}) @@ -135,20 +135,22 @@ module.exports = function(options) { }, } $window.history = { - pushState: function(data, title, url) { - past.push({url: getURL(), isNew: false}) + pushState: function(state, title, url) { + past.push({url: getURL(), isNew: false, state: state, title: title}) future = [] setURL(url) }, - replaceState: function(data, title, url) { - future = [] + replaceState: function(state, title, url) { + var entry = past[past.length - 1] + entry.state = state + entry.title = title setURL(url) }, back: function() { - var entry = past.pop() - if (entry != null) { + if (past.length > 1) { + var entry = past.pop() if (entry.isNew) unload() - future.push({url: getURL(), isNew: false}) + future.push({url: getURL(), isNew: false, state: entry.state, title: entry.title}) setURL(entry.url) if (!entry.isNew) popstate() } @@ -157,11 +159,14 @@ module.exports = function(options) { var entry = future.pop() if (entry != null) { if (entry.isNew) unload() - past.push({url: getURL(), isNew: false}) + past.push({url: getURL(), isNew: false, state: entry.state, title: entry.title}) setURL(entry.url) if (!entry.isNew) popstate() } }, + get state() { + return past.length === 0 ? null : past[past.length - 1].state + }, } $window.onpopstate = null, $window.onhashchange = null, diff --git a/test-utils/tests/test-pushStateMock.js b/test-utils/tests/test-pushStateMock.js index fa9c064f..86298b30 100644 --- a/test-utils/tests/test-pushStateMock.js +++ b/test-utils/tests/test-pushStateMock.js @@ -417,6 +417,71 @@ o.spec("pushStateMock", function() { $window.history.pushState(null, null, "b") $window.history.back() }) + o("replaceState does not break forward history", function() { + $window.onpopstate = o.spy() + + $window.history.pushState(null, null, "b") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + o($window.location.href).equals("http://localhost/") + + $window.history.replaceState(null, null, "a") + + o($window.location.href).equals("http://localhost/a") + + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + o($window.location.href).equals("http://localhost/b") + }) + o("pushstate retains state", function() { + $window.onpopstate = o.spy() + + $window.history.pushState({a: 1}, null, "#a") + $window.history.pushState({b: 2}, null, "#b") + + o($window.onpopstate.callCount).equals(0) + + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + o($window.onpopstate.args[0].type).equals("popstate") + o($window.onpopstate.args[0].state).deepEquals({a: 1}) + + $window.history.back() + + o($window.onpopstate.callCount).equals(2) + o($window.onpopstate.args[0].type).equals("popstate") + o($window.onpopstate.args[0].state).equals(null) + + $window.history.forward() + + o($window.onpopstate.callCount).equals(3) + o($window.onpopstate.args[0].type).equals("popstate") + o($window.onpopstate.args[0].state).deepEquals({a: 1}) + + $window.history.forward() + + o($window.onpopstate.callCount).equals(4) + o($window.onpopstate.args[0].type).equals("popstate") + o($window.onpopstate.args[0].state).deepEquals({b: 2}) + }) + o("replacestate replaces state", function() { + $window.onpopstate = o.spy(pop) + + $window.history.replaceState({a: 1}, null, "a") + + o($window.history.state).deepEquals({a: 1}) + + $window.history.pushState(null, null, "a") + $window.history.back() + + function pop(e) { + o(e.state).deepEquals({a: 1}) + o($window.history.state).deepEquals({a: 1}) + } + }) }) o.spec("onhashchance", function() { o("onhashchange triggers on location.href change", function() { From 15c28066d7b8a1a702144ec37aecd2bd4b8b68b9 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 18 Dec 2016 22:07:33 +1100 Subject: [PATCH 10/54] Spy on setAttribute to test customElement support --- render/render.js | 7 ++-- render/tests/test-attributes.js | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/render/render.js b/render/render.js index 8d9bd366..731ebdf3 100644 --- a/render/render.js +++ b/render/render.js @@ -435,14 +435,14 @@ module.exports = function($window) { } function setAttr(vnode, key, old, value, ns) { var element = vnode.dom - if (key === "key" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return + if (key === "key" || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return var nsLastIndex = key.indexOf(":") if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") { element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value) } else if (key[0] === "o" && key[1] === "n" && typeof value === "function") updateEvent(vnode, key, value) else if (key === "style") updateStyle(element, old, value) - else if (key in element && !isAttribute(key) && ns === undefined) { + else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome if (vnode.tag === "input" && key === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome @@ -491,6 +491,9 @@ module.exports = function($window) { function isAttribute(attr) { return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type" } + function isCustomElement(vnode){ + return vnode.attrs.is || vnode.tag.indexOf("-") > -1 + } function hasIntegrationMethods(source) { return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove) } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 73cbd685..8159d0d3 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -11,7 +11,68 @@ o.spec("attributes", function() { root = $window.document.body render = vdom($window).render }) + o.spec("customElements", function(){ + + o("when vnode is customElement, custom setAttribute called", function(){ + var normalFirstDraw = [ + { tag: "input", attrs: { wow: 'text' } }, + { tag: "input", attrs: { wow: 'text' } }, + { tag: "input", attrs: { wow: 'text' } }, + ] + + var normalSecondDraw = [ + { tag: "input", attrs: { wow: 'text' } }, + { tag: "input", attrs: { wow: 'text' } }, + { tag: "input", attrs: { wow: 'text' } }, + ] + + var customFirstDraw = [ + { tag: "custom-element", attrs: { custom: 'x' } }, + { tag: "input", attrs: { is: 'something-special', custom: 'x' } }, + { tag: "custom-element", attrs: { is: 'something-special', custom: 'x' } } + ] + + var customSecondDraw = [ + { tag: "custom-element", attrs: { custom: 'y' } }, + { tag: "input", attrs: { is: 'something-special', custom: 'y' } }, + { tag: "custom-element", attrs: { is: 'something-special', custom: 'y' } } + ] + + var draws = [ + normalFirstDraw, normalSecondDraw, + customFirstDraw, customSecondDraw + ] + + var customRedraws = 2 + var customSetAttrCalls = customFirstDraw.length * customRedraws; + var innerHTMLCalls = normalFirstDraw.length + + var f = $window.document.createElement + var spy + + $window.document.createElement = function(tag, is){ + var el = f(tag, is) + if(!spy){ + spy = o.spy(el.setAttribute) + } + el.setAttribute = spy + + return el + } + + draws.forEach( + function(view) { + render(root, view) + } + ) + + o(spy.callCount).equals( + customSetAttrCalls + innerHTMLCalls + ) + }) + + }) o.spec("input readonly", function() { o("when input readonly is true, attribute is present", function() { var a = {tag: "input", attrs: {readonly: true}} From 39b6f1e0feddcb97ec43e89e3d50e9da83d80911 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 18 Dec 2016 22:21:53 +1100 Subject: [PATCH 11/54] Simplify draw routine --- render/tests/test-attributes.js | 43 +++++++-------------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 8159d0d3..2d08007b 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -15,38 +15,19 @@ o.spec("attributes", function() { o("when vnode is customElement, custom setAttribute called", function(){ - var normalFirstDraw = [ - { tag: "input", attrs: { wow: 'text' } }, - { tag: "input", attrs: { wow: 'text' } }, - { tag: "input", attrs: { wow: 'text' } }, + var normal = [ + { tag: "input", attrs: { value: 'hello' } }, + { tag: "input", attrs: { value: 'hello' } }, + { tag: "input", attrs: { value: 'hello' } } ] - - var normalSecondDraw = [ - { tag: "input", attrs: { wow: 'text' } }, - { tag: "input", attrs: { wow: 'text' } }, - { tag: "input", attrs: { wow: 'text' } }, - ] - - var customFirstDraw = [ + + var custom = [ { tag: "custom-element", attrs: { custom: 'x' } }, { tag: "input", attrs: { is: 'something-special', custom: 'x' } }, { tag: "custom-element", attrs: { is: 'something-special', custom: 'x' } } ] - var customSecondDraw = [ - { tag: "custom-element", attrs: { custom: 'y' } }, - { tag: "input", attrs: { is: 'something-special', custom: 'y' } }, - { tag: "custom-element", attrs: { is: 'something-special', custom: 'y' } } - ] - - var draws = [ - normalFirstDraw, normalSecondDraw, - customFirstDraw, customSecondDraw - ] - - var customRedraws = 2 - var customSetAttrCalls = customFirstDraw.length * customRedraws; - var innerHTMLCalls = normalFirstDraw.length + var view = normal.concat(custom) var f = $window.document.createElement var spy @@ -61,15 +42,9 @@ o.spec("attributes", function() { return el } - draws.forEach( - function(view) { - render(root, view) - } - ) + render(root, view) - o(spy.callCount).equals( - customSetAttrCalls + innerHTMLCalls - ) + o(spy.callCount).equals( custom.length ) }) }) From 0bab044bba9221579d4835e6ad0b71a7a489e360 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sun, 18 Dec 2016 15:34:16 +0000 Subject: [PATCH 12/54] Bundled output for commit 23ce6031299aebf58d101bb4d5477ad51f2da162 [skip ci] --- README.md | 2 +- mithril.js | 7 +++++-- mithril.min.js | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ac65b7f1..9664d8f4 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.53 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.54 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 43523644..715ecc63 100644 --- a/mithril.js +++ b/mithril.js @@ -759,14 +759,14 @@ var coreRenderer = function($window) { } function setAttr(vnode, key2, old, value, ns) { var element = vnode.dom - if (key2 === "key" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return + if (key2 === "key" || key2 === "is" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return var nsLastIndex = key2.indexOf(":") if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") { element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value) } else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value) else if (key2 === "style") updateStyle(element, old, value) - else if (key2 in element && !isAttribute(key2) && ns === undefined) { + else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome if (vnode.tag === "input" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome @@ -815,6 +815,9 @@ var coreRenderer = function($window) { function isAttribute(attr) { return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type" } + function isCustomElement(vnode){ + return vnode.attrs.is || vnode.tag.indexOf("-") > -1 + } function hasIntegrationMethods(source) { return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove) } diff --git a/mithril.min.js b/mithril.min.js index 5242bdcd..811aaac4 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -26,16 +26,16 @@ null!=b.text&&(b.children=[u("#",void 0,void 0,b.text,void 0,void 0)]),g(q,f.chi b.dom){var e=y.createDocumentFragment();if(0 Date: Tue, 20 Dec 2016 11:15:10 -0500 Subject: [PATCH 13/54] beef up the docs about differences between route resolvers and components --- docs/route.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/route.md b/docs/route.md index 5ce6d1cd..c443d418 100644 --- a/docs/route.md +++ b/docs/route.md @@ -390,31 +390,76 @@ In the example above, the layout merely consists of a `
` tha One way to wrap the layout is to define an anonymous component in the routes map: ```javascript +// example 1 m.route(document.body, "/", { "/": { view: function() { return m(Layout, m(Home)) }, + }, + "/form": { + view: function() { + return m(Layout, m(Form)) + }, } }) ``` -However, note that because the top level component is an anonymous component, jumping from route to route will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable. +However, note that because the top level component is an anonymous component, jumping from the `/` route to the `/form` route (or vice-versa) will tear down the anonymous component and recreate the DOM from scratch. If the Layout component had [lifecycle methods](lifecycle-methods.md) defined, the `oninit` and `oncreate` hooks would fire on every route change. Depending on the application, this may or may not be desirable. If you would prefer to have the Layout component be diffed and maintained intact rather than recreated from scratch, you should instead use a RouteResolver as the root object: ```javascript +// example 2 m.route(document.body, "/", { "/": { render: function() { return m(Layout, m(Home)) }, + }, + "/form": { + render: function() { + return m(Layout, m(Form)) + }, } }) ``` Note that in this case, if the Layout component the `oninit` and `oncreate` lifecycle methods would only fire on the Layout component on the first route change (assuming all routes use the same layout). +To clarify the difference between the two examples, example 1 is equivalent to this code: + +```javascript +// functionally equivalent to example 1 +var Anon1 = { + view: function() { + return m(Layout, m(Home)) + }, +} +var Anon2 = { + view: function() { + return m(Layout, m(Form)) + }, +} + +m.route(document.body, "/", { + "/": { + render: function() { + return m(Anon1) + } + }, + "/form": { + render: function() { + return m(Anon2) + } + }, +}) +``` + +Since `Anon1` and `Anon2` are different components, their subtrees (including `Layout`) are recreated from scratch. This is also what happens when components are used directly without a RouteResolver. + +In example 2, since `Layout` is the top-level component in both routes, the DOM for the `Layout` component is diffed (i.e. left intact if it has no changes), and only the change from `Home` to `Form` triggers a recreation of that subsection of the DOM. + --- #### Authentication From d1a3f7fe9b5c2598cfe021ad133527cec1cacdc0 Mon Sep 17 00:00:00 2001 From: Mikkel Bergmann Date: Wed, 21 Dec 2016 10:02:21 +1100 Subject: [PATCH 14/54] Make it work in a browser --- stream/stream.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream/stream.js b/stream/stream.js index 9a29ab02..4a51f9e2 100644 --- a/stream/stream.js +++ b/stream/stream.js @@ -112,4 +112,5 @@ createStream.merge = merge createStream.combine = combine createStream.HALT = HALT -module.exports = createStream +if (typeof module !== "undefined") module["exports"] = createStream +else window.stream = createStream From f72aa50f27b0bc29144b776b7066106ef7239646 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 20 Dec 2016 21:19:16 -0800 Subject: [PATCH 15/54] Expose `Vnode` as `m.vnode` (#1487) --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index d849a317..6e69cea4 100644 --- a/index.js +++ b/index.js @@ -16,5 +16,6 @@ m.jsonp = requestService.jsonp m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.version = "bleeding-edge" +m.vnode = require("./render/vnode") module.exports = m From bc01dad847f02aa1636fe1aba6057d6b338f02d6 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 21 Dec 2016 05:20:06 +0000 Subject: [PATCH 16/54] Bundled output for commit f72aa50f27b0bc29144b776b7066106ef7239646 [skip ci] --- README.md | 2 +- mithril.js | 1 + mithril.min.js | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9664d8f4..913fdfe3 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.54 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.55 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 715ecc63..a27e1734 100644 --- a/mithril.js +++ b/mithril.js @@ -1153,6 +1153,7 @@ m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString m.version = "1.0.0-rc.6" +m.vnode = Vnode if (typeof module !== "undefined") module["exports"] = m else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 811aaac4..f31212e4 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function u(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 A(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===J[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,"\\")), +new function(){function u(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===J[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(C){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(E){d(E)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?w:t(w)},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){t=a}}}(window, -G),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&w>=B;){var v=f[k],q=a[B];if(v!==q||r)if(null==v)k++;else if(null==q)B++;else if(v.key===q.key)k++,B++,m(e,v,q,b,n(f,k,d),r,g),r&&v.tag===q.tag&&p(e,l(v),d);else if(v=f[x],v!==q||r)if(null==v)x--;else if(null==q)B++;else if(v.key===q.key)m(e,v,q,b,n(f,x+1,d),r,g),(r||B=k&&w>=B;){v=f[x];q=a[w]; if(v!==q||r)if(null==v)x--;else{if(null!=q)if(v.key===q.key)m(e,v,q,b,n(f,x+1,d),r,g),r&&v.tag===q.tag&&p(e,l(v),d),null!=v.dom&&(d=v.dom),x--;else{if(!C){C=f;var v=x,y={},F;for(F=0;F Date: Wed, 21 Dec 2016 23:34:44 -0700 Subject: [PATCH 17/54] Fix #1489: PromisePolyfill always overwrites `window.Promise` --- promise/promise.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/promise/promise.js b/promise/promise.js index f24766d1..53aefecc 100644 --- a/promise/promise.js +++ b/promise/promise.js @@ -95,9 +95,12 @@ PromisePolyfill.race = function(list) { }) } -if (typeof Promise === "undefined") { - if (typeof window !== "undefined") window.Promise = PromisePolyfill - else if (typeof global !== "undefined") global.Promise = PromisePolyfill +if (typeof window !== "undefined") { + if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill + module.exports = window.Promise +} else if (typeof global !== "undefined") { + if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill + module.exports = global.Promise +} else { + module.exports = PromisePolyfill } - -module.exports = typeof Promise !== "undefined" ? Promise : PromisePolyfill \ No newline at end of file From b6daeaab5aae50f45610e89ed9cec9e860b3c44f Mon Sep 17 00:00:00 2001 From: Bryce Gibson Date: Thu, 22 Dec 2016 21:12:50 +1100 Subject: [PATCH 18/54] Fix back button for default route. --- api/router.js | 2 +- mithril.js | 2 +- mithril.min.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/router.js b/api/router.js index ccfccb94..6f805de2 100644 --- a/api/router.js +++ b/api/router.js @@ -15,7 +15,7 @@ module.exports = function($window, redrawService) { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } var bail = function() { - routeService.setPath(defaultRoute) + routeService.setPath(defaultRoute, null, { replace: true }) } routeService.defineRoutes(routes, function(payload, params, path) { var update = lastUpdate = function(routeResolver, comp) { diff --git a/mithril.js b/mithril.js index a27e1734..104a4dac 100644 --- a/mithril.js +++ b/mithril.js @@ -1098,7 +1098,7 @@ var _20 = function($window, redrawService0) { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } var bail = function() { - routeService.setPath(defaultRoute) + routeService.setPath(defaultRoute, null, { replace: true }) } routeService.defineRoutes(routes, function(payload, params, path) { var update = lastUpdate = function(routeResolver, comp) { diff --git a/mithril.min.js b/mithril.min.js index f31212e4..dc602474 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -36,6 +36,6 @@ decodeURIComponent(g[1]):"";"true"===g?g=!0:"false"===g&&(g=!1);var l=m.split(/\ l&&(l=m(function(){l=null;a()}))}}function d(a,c,d){var b=a.indexOf("?"),g=a.indexOf("#"),k=-1 Date: Thu, 22 Dec 2016 21:14:52 +1100 Subject: [PATCH 19/54] Test precondition. --- api/tests/test-router.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 7e2454cb..c8bbfac1 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -78,6 +78,8 @@ o.spec("route", function() { callAsync(function() { o(root.firstChild.nodeName).equals("DIV") + o($window.location.pathname).equals("/a") + $window.history.back() o($window.location.pathname).equals("/") From f4084671bccdc6efe3638512166515307e0b5f36 Mon Sep 17 00:00:00 2001 From: Bryce Gibson Date: Thu, 22 Dec 2016 21:15:15 +1100 Subject: [PATCH 20/54] Typo. --- api/tests/test-router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index c8bbfac1..47bc1ac5 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -51,7 +51,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("routed mount points can redraw synchronoulsy (#1275)", function() { + o("routed mount points can redraw synchronously (#1275)", function() { var view = o.spy() $window.location.href = prefix + "/" From d42d728681efcaa84851b1131b3d42dc483514af Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 23 Dec 2016 08:52:09 -0500 Subject: [PATCH 21/54] fix test --- api/tests/test-router.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 47bc1ac5..66968a77 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -78,7 +78,7 @@ o.spec("route", function() { callAsync(function() { o(root.firstChild.nodeName).equals("DIV") - o($window.location.pathname).equals("/a") + o(route.get()).equals("/a") $window.history.back() @@ -576,7 +576,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(2) - + done() }) }) @@ -611,7 +611,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(2) - + done() }) }) @@ -619,7 +619,7 @@ 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" : { @@ -970,7 +970,7 @@ o.spec("route", function() { o(renderA.callCount).equals(0) o(renderB.callCount).equals(1) - done() + done() }) }, 20) }) @@ -1192,19 +1192,19 @@ o.spec("route", function() { } }, }) - + 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() }) }) }) }) - + o("throttles", function(done, timeout) { timeout(200) From eadd9a07680d8ba34b1a6b2091d9439172e89c67 Mon Sep 17 00:00:00 2001 From: Bryce Gibson Date: Sat, 24 Dec 2016 09:36:24 +1100 Subject: [PATCH 22/54] Ensure the test checks the right thing. --- api/tests/test-router.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 66968a77..d70a7c23 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -66,7 +66,9 @@ o.spec("route", function() { }) o("default route doesn't break back button", function(done) { - $window.location.href = "http://google.com" + $window.location.href = "http://old.com" + $window.location.href = "http://new.com" + route(root, "/a", { "/a" : { view: function() { @@ -83,6 +85,7 @@ o.spec("route", function() { $window.history.back() o($window.location.pathname).equals("/") + o($window.location.hostname).equals("old.com") done() }) From 64b759fd83826d4cff56d330361eb1f31f431a1b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 26 Dec 2016 11:02:41 +0100 Subject: [PATCH 23/54] [ospec] Speed up the CLI runner by skipping node_modules early --- ospec/bin/ospec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ospec/bin/ospec b/ospec/bin/ospec index 49b3ddc0..45c00a35 100644 --- a/ospec/bin/ospec +++ b/ospec/bin/ospec @@ -15,6 +15,7 @@ function traverseDirectory(pathname, callback) { if (err) reject(err) var promises = [] for (var i = 0; i < pathnames.length; i++) { + if (pathnames[i] === "node_modules") continue pathnames[i] = path.join(pathname, pathnames[i]) promises.push(traverseDirectory(pathnames[i], callback)) } @@ -31,7 +32,6 @@ function traverseDirectory(pathname, callback) { } traverseDirectory(".", function(pathname, stat, children) { - if (pathname.indexOf("node_modules") > -1) return if (pathname.match(/(?:^|\/)tests\/.*\.js$/)) { require(path.normalize(process.cwd()) + "/" + pathname) } From e8f36e4e268a26ae08fe348243137c1c35f55a0d Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Mon, 26 Dec 2016 12:18:51 -0500 Subject: [PATCH 24/54] don't break back button on bail --- api/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/router.js b/api/router.js index ccfccb94..cf406737 100644 --- a/api/router.js +++ b/api/router.js @@ -15,7 +15,7 @@ module.exports = function($window, redrawService) { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } var bail = function() { - routeService.setPath(defaultRoute) + routeService.setPath(defaultRoute, null, {replace: true}) } routeService.defineRoutes(routes, function(payload, params, path) { var update = lastUpdate = function(routeResolver, comp) { From 1222a9a3ab1374d327d9a3d067af5d519547dd82 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Mon, 26 Dec 2016 12:19:20 -0500 Subject: [PATCH 25/54] change onbeforeremove and make state inherit from prototype --- render/render.js | 22 ++++---- render/tests/index.html | 1 + render/tests/test-component.js | 12 ++--- render/tests/test-onbeforeremove.js | 82 ++++++++--------------------- 4 files changed, 41 insertions(+), 76 deletions(-) diff --git a/render/render.js b/render/render.js index 731ebdf3..c71177e9 100644 --- a/render/render.js +++ b/render/render.js @@ -97,7 +97,9 @@ module.exports = function($window) { function createComponent(vnode, hooks, ns) { // For object literals since `Vnode()` always sets the `state` field. if (!vnode.state) vnode.state = {} - assign(vnode.state, vnode.tag) + var constructor = function() {} + constructor.prototype = vnode.tag + vnode.state = new constructor var view = vnode.tag.view if (view.reentrantLock != null) return $emptyFragment @@ -380,12 +382,18 @@ module.exports = function($window) { function removeNode(vnode, context) { var expected = 1, called = 0 if (vnode.attrs && vnode.attrs.onbeforeremove) { - expected++ - vnode.attrs.onbeforeremove.call(vnode.state, vnode, once(continuation)) + var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) { - expected++ - vnode.tag.onbeforeremove.call(vnode.state, vnode, once(continuation)) + var result = vnode.tag.onbeforeremove.call(vnode.state, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } continuation() function continuation() { @@ -559,10 +567,6 @@ module.exports = function($window) { return false } - function assign(target, source) { - Object.keys(source).forEach(function(k){target[k] = source[k]}) - } - function render(dom, vnodes) { if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] diff --git a/render/tests/index.html b/render/tests/index.html index 3cc2da65..480b8b7c 100644 --- a/render/tests/index.html +++ b/render/tests/index.html @@ -14,6 +14,7 @@ + diff --git a/render/tests/test-component.js b/render/tests/test-component.js index d6d26e43..f4aad629 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -588,14 +588,12 @@ o.spec("component", function() { o("calls onbeforeremove", function() { var called = 0 var component = { - onbeforeremove: function(vnode, done) { + onbeforeremove: function(vnode) { called++ o(vnode.dom).notEquals(undefined) o(vnode.dom).equals(root.firstChild) o(root.childNodes.length).equals(1) - - done() }, view: function() { return {tag: "div", attrs: {id: "a"}, text: "b"} @@ -614,14 +612,12 @@ o.spec("component", function() { o("calls onbeforeremove when returning fragment", function() { var called = 0 var component = { - onbeforeremove: function(vnode, done) { + onbeforeremove: function(vnode) { called++ o(vnode.dom).notEquals(undefined) o(vnode.dom).equals(root.firstChild) o(root.childNodes.length).equals(1) - - done() }, view: function() { return [{tag: "div", attrs: {id: "a"}, text: "b"}] @@ -672,6 +668,10 @@ o.spec("component", function() { function init(vnode) { o(vnode.state.data).deepEquals(data) o(vnode.state.data).equals(data) + + //inherits state via prototype + component.x = 1 + o(vnode.state.x).equals(1) } }) o("state copy is shallow", function() { diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 84e9077c..82a859d0 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -4,6 +4,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") +var Promise = require("../../promise/promise") o.spec("onbeforeremove", function() { var $window, root, render @@ -43,17 +44,13 @@ o.spec("onbeforeremove", function() { render(root, [vnode]) render(root, []) - function remove(node, complete) { + function remove(node) { o(node).equals(vnode) o(this).equals(vnode.state) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) callAsync(function() { - o(root.childNodes.length).equals(1) - - complete() - o(root.childNodes.length).equals(0) done() @@ -66,16 +63,12 @@ o.spec("onbeforeremove", function() { render(root, [vnode]) render(root, []) - function remove(node, complete) { + function remove(node) { o(node).equals(vnode) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) callAsync(function() { - o(root.childNodes.length).equals(1) - - complete() - o(root.childNodes.length).equals(0) done() @@ -88,16 +81,12 @@ o.spec("onbeforeremove", function() { render(root, [vnode]) render(root, []) - function remove(node, complete) { + function remove(node) { o(node).equals(vnode) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) callAsync(function() { - o(root.childNodes.length).equals(1) - - complete() - o(root.childNodes.length).equals(0) done() @@ -110,16 +99,12 @@ o.spec("onbeforeremove", function() { render(root, [vnode]) render(root, []) - function remove(node, complete) { + function remove(node) { o(node).equals(vnode) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) callAsync(function() { - o(root.childNodes.length).equals(1) - - complete() - o(root.childNodes.length).equals(0) done() @@ -133,17 +118,12 @@ o.spec("onbeforeremove", function() { render(root, [vnode]) render(root, []) - function remove(node, complete) { + function remove(node) { o(node).equals(vnode) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) callAsync(function() { - o(root.childNodes.length).equals(1) - o(spy.callCount).equals(0) - - complete() - o(root.childNodes.length).equals(0) o(spy.callCount).equals(1) @@ -161,7 +141,7 @@ o.spec("onbeforeremove", function() { o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) }) o("does not recycle when there's an onbeforeremove", function() { - var remove = function(vnode, done) {done()} + var remove = function(vnode) {} var vnode = {tag: "div", key: 1, attrs: {onbeforeremove: remove}} var updated = {tag: "div", key: 1, attrs: {onbeforeremove: remove}} @@ -171,9 +151,8 @@ o.spec("onbeforeremove", function() { o(vnode.dom).notEquals(updated.dom) }) - o("does not leave elements out of order during removal", function() { - var finish - var remove = function(vnode, done) {finish = done} + o("does not leave elements out of order during removal", function(done) { + var remove = function(vnode) {return Promise.resolve()} var vnodes = [{tag: "div", key: 1, attrs: {onbeforeremove: remove}, text: "1"}, {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"}] var updated = {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"} @@ -183,14 +162,16 @@ o.spec("onbeforeremove", function() { o(root.childNodes.length).equals(2) o(root.firstChild.firstChild.nodeValue).equals("1") - finish() - - o(root.childNodes.length).equals(1) - o(root.firstChild.firstChild.nodeValue).equals("2") + callAsync(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.firstChild.nodeValue).equals("2") + + done() + }) }) - o("finalizes the remove phase only once when `done()` is called synchronously from both attrs- and tag.onbeforeremove", function() { + o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) { var onremove = o.spy() - var onbeforeremove = function(vnode, done){done()} + var onbeforeremove = function(){return Promise.resolve()} var component = { onbeforeremove: onbeforeremove, onremove: onremove, @@ -198,30 +179,9 @@ o.spec("onbeforeremove", function() { } render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}]) render(root, []) - o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - }) - o("doesn't finalize prematurely if `done` is called twice in the `tag` hook", function(done) { - var async = false - var component = { - view: function() {}, - onbeforeremove: function(vnode, doneRemoving){ - doneRemoving() - doneRemoving() - }, - onremove: function() { - o(async).equals(true) - done() - }, - } - render(root, [{ - tag:component, - attrs: { - onbeforeremove: function(vnode, doneRemoving){ - callAsync(doneRemoving) - } - } - }]) - render(root, []) - async = true + callAsync(function() { + o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` + done() + }) }) }) From 9ff3e46ab34dbed7b98d302461b162d2f4bbed74 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 26 Dec 2016 17:22:19 +0000 Subject: [PATCH 26/54] Bundled output for commit 78b9a082acf42bf5575333ec5dd3e717214ebf3e [skip ci] --- mithril.js | 23 ++++++++------ mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/mithril.js b/mithril.js index 104a4dac..b5d5203c 100644 --- a/mithril.js +++ b/mithril.js @@ -428,7 +428,9 @@ var coreRenderer = function($window) { function createComponent(vnode, hooks, ns) { // For object literals since `Vnode()` always sets the `state` field. if (!vnode.state) vnode.state = {} - assign(vnode.state, vnode.tag) + var constructor = function() {} + constructor.prototype = vnode.tag + vnode.state = new constructor var view = vnode.tag.view if (view.reentrantLock != null) return $emptyFragment view.reentrantLock = true @@ -705,12 +707,18 @@ var coreRenderer = function($window) { function removeNode(vnode, context) { var expected = 1, called = 0 if (vnode.attrs && vnode.attrs.onbeforeremove) { - expected++ - vnode.attrs.onbeforeremove.call(vnode.state, vnode, once(continuation)) + var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) { - expected++ - vnode.tag.onbeforeremove.call(vnode.state, vnode, once(continuation)) + var result = vnode.tag.onbeforeremove.call(vnode.state, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } continuation() function continuation() { @@ -879,9 +887,6 @@ var coreRenderer = function($window) { } return false } - function assign(target, source) { - Object.keys(source).forEach(function(k){target[k] = source[k]}) - } function render(dom, vnodes) { if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] @@ -1098,7 +1103,7 @@ var _20 = function($window, redrawService0) { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } var bail = function() { - routeService.setPath(defaultRoute, null, { replace: true }) + routeService.setPath(defaultRoute, null, {replace: true}) } routeService.defineRoutes(routes, function(payload, params, path) { var update = lastUpdate = function(routeResolver, comp) { diff --git a/mithril.min.js b/mithril.min.js index dc602474..a9424e7a 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function u(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===J[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(C){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(E){d(E)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?w:t(w)},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){t=a}}}(window, -G),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&w>=B;){var v=f[k],q=a[B];if(v!==q||r)if(null==v)k++;else if(null==q)B++;else if(v.key===q.key)k++,B++,m(e,v,q,b,n(f,k,d),r,g),r&&v.tag===q.tag&&p(e,l(v),d);else if(v=f[x],v!==q||r)if(null==v)x--;else if(null==q)B++;else if(v.key===q.key)m(e,v,q,b,n(f,x+1,d),r,g),(r||B=k&&w>=B;){v=f[x];q=a[w]; -if(v!==q||r)if(null==v)x--;else{if(null!=q)if(v.key===q.key)m(e,v,q,b,n(f,x+1,d),r,g),r&&v.tag===q.tag&&p(e,l(v),d),null!=v.dom&&(d=v.dom),x--;else{if(!C){C=f;var v=x,y={},F;for(F=0;Fa.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(C){throw Error(a);}}function r(a){return a.responseText}function q(a, +c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bm.status||304===m.status)c(q(b.type,a));else{var f=Error(m.responseText),g;for(g in a)f[g]=a[g];d(f)}}catch(e){d(e)}};g&&null!=b.data?m.send(b.data):m.send()});return!0===b.background?y:u(y)},jsonp:function(b,k){var r=g();b=d(b,k);var u=new c(function(c, +d){var g=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[g]=function(d){k.parentNode.removeChild(k);c(q(b.type,d));delete a[g]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[g]};null==b.data&&(b.data={});b.url=f(b.url,b.data);b.data[b.callbackKey||"callback"]=g;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?u:r(u)},setCompletionCallback:function(a){u=a}}}(window, +F),O=function(a){function c(e,h,a,b,c,d,f){for(;a=t&&y>=u;){var x=a[t],n=p[u];if(x!==n||h)if(null==x)t++;else if(null==n)u++;else if(x.key===n.key)t++,u++,k(e,x,n,d,q(a,t,f),h,l),h&&x.tag===n.tag&&m(e,r(x),f);else if(x=a[v],x!==n||h)if(null==x)v--;else if(null==n)u++;else if(x.key===n.key)k(e,x,n,d,q(a,v+1,f),h,l),(h||u=t&&y>=u;){x=a[v];n=p[y];if(x!==n||h)if(null==x)v--;else{if(null!=n)if(x.key===n.key)k(e,x,n,d,q(a,v+1,f),h,l),h&&x.tag===n.tag&&m(e,r(x),f),null!=x.dom&&(f=x.dom),v--;else{if(!C){C=a;var x=v,E={},w;for(w=0;w Date: Mon, 26 Dec 2016 12:28:13 -0500 Subject: [PATCH 27/54] update docs re:onbeforeremove and clean up --- docs/change-log.md | 4 ++-- docs/components.md | 7 +++++-- docs/hyperscript.md | 4 ++-- docs/lifecycle-methods.md | 8 +++++--- render/render.js | 9 --------- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 0475c996..b67fb4c7 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -110,8 +110,8 @@ m("div", { onbeforeupdate : function(vnode, old) { /*...*/ }, // Called after the node is updated onupdate : function(vnode) { /*...*/ }, - // Called before the node is removed, call done() when ready for the node to be removed from the DOM - onbeforeremove : function(vnode, done) { /*...*/ }, + // Called before the node is removed, return a Promise that resolves when ready for the node to be removed from the DOM + onbeforeremove : function(vnode) { /*...*/ }, // Called before the node is removed, but after onbeforeremove calls done() onremove : function(vnode) { /*...*/ } }); diff --git a/docs/components.md b/docs/components.md index ee74bd5c..47f8d1c3 100644 --- a/docs/components.md +++ b/docs/components.md @@ -63,9 +63,12 @@ var ComponentWithHooks = { onupdate: function(vnode) { console.log("DOM updated") }, - onbeforeremove: function(vnode, done) { + onbeforeremove: function(vnode) { console.log("exit animation can start") - done() + return new Promise(function(resolve) { + // call after animation completes + resolve() + }) }, onremove: function(vnode) { console.log("removing DOM element") diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 9d608dcd..6d27a4e6 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -249,9 +249,9 @@ Hook | Description `oninit(vnode)` | Runs before a vnode is rendered into a real DOM element `oncreate(vnode)` | Runs after a vnode is appended to the DOM `onupdate(vnode)` | Runs every time a redraw occurs while the DOM element is attached to the document -`onbeforeremove(vnode, done)` | Runs before a DOM element is removed from the document, and only triggers the actual removal of the DOM element when the `done` callback is called. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements. +`onbeforeremove(vnode)` | Runs before a DOM element is removed from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements. `onremove(vnode)` | Runs before a DOM element is removed from the document. If a `onbeforeremove` hook is defined, `onremove` is called after `done` is called. This method is triggered on the element that is detached from its parent element, and all of its children -`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children +`onbeforeupdate(vnode, old)` | Runs before `onupdate` and if it returns `false`, it prevents a diff for the element and all of its children To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md). diff --git a/docs/lifecycle-methods.md b/docs/lifecycle-methods.md index 72c2c394..af78738f 100644 --- a/docs/lifecycle-methods.md +++ b/docs/lifecycle-methods.md @@ -130,7 +130,7 @@ m(RedrawReporter, {data: "Hello"}) ### onbeforeremove -The `onbeforeremove(vnode, done)` hook is called before a DOM element is detached from the document. Mithril only detaches the DOM element after the `done` callback is called. The `done` callback can be called asynchronously, making it possible to run exit animations before detaching the element. +The `onbeforeremove(vnode)` hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. This hook is only called on the DOM element that loses its `parentNode`, but it does not get called in its child elements. @@ -138,9 +138,11 @@ Like in other hooks, the `this` keyword in the `onbeforeremove` callback points ```javascript var Fader = { - onbeforeremove: function(vnode, done) { + onbeforeremove: function(vnode) { vnode.dom.classList.add("fade-out") - setTimeout(done, 1000) + return new Promise(function(resolve) { + setTimeout(resolve, 1000) + }) }, view: function() { return m("div", "Bye") diff --git a/render/render.js b/render/render.js index c71177e9..516b0c7f 100644 --- a/render/render.js +++ b/render/render.js @@ -370,15 +370,6 @@ module.exports = function($window) { } } } - function once(f) { - var called = false - return function() { - if (!called) { - called = true - f() - } - } - } function removeNode(vnode, context) { var expected = 1, called = 0 if (vnode.attrs && vnode.attrs.onbeforeremove) { From 4da9e8891e1d7611e0b88badeaaaa8101fb77c53 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 26 Dec 2016 17:29:20 +0000 Subject: [PATCH 28/54] Bundled output for commit c0f48f57efd1d0beba717d356896b20eff3425f1 [skip ci] --- mithril.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mithril.js b/mithril.js index b5d5203c..9677ba16 100644 --- a/mithril.js +++ b/mithril.js @@ -695,15 +695,6 @@ var coreRenderer = function($window) { } } } - function once(f) { - var called = false - return function() { - if (!called) { - called = true - f() - } - } - } function removeNode(vnode, context) { var expected = 1, called = 0 if (vnode.attrs && vnode.attrs.onbeforeremove) { From 7646f607af6fdaf3a211dd1902e063d8e366c2b9 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 26 Dec 2016 19:33:00 +0000 Subject: [PATCH 29/54] Bundled output for commit b7ccabfe156b396ecf6c903012169d50c766f1e9 [skip ci] --- mithril.js | 39 +++++++++++++----------- mithril.min.js | 83 +++++++++++++++++++++++++------------------------- 2 files changed, 63 insertions(+), 59 deletions(-) diff --git a/mithril.js b/mithril.js index 9677ba16..f4c77a2e 100644 --- a/mithril.js +++ b/mithril.js @@ -82,8 +82,8 @@ hyperscript.fragment = function(attrs1, children) { } var m = hyperscript /** @constructor */ -var PromisePolyfill0 = function(executor) { - if (!(this instanceof PromisePolyfill0)) throw new Error("Promise must be called with `new`") +var PromisePolyfill = function(executor) { + if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`") if (typeof executor !== "function") throw new TypeError("executor must be a function") var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} @@ -124,7 +124,7 @@ var PromisePolyfill0 = function(executor) { } executeOnce(executor) } -PromisePolyfill0.prototype.then = function(onFulfilled, onRejection) { +PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { var self = this, instance = self._instance function handle(callback, list, next, state) { list.push(function(value) { @@ -134,22 +134,22 @@ PromisePolyfill0.prototype.then = function(onFulfilled, onRejection) { if (typeof instance.retry === "function" && state === instance.state) instance.retry() } var resolveNext, rejectNext - var promise = new PromisePolyfill0(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) return promise } -PromisePolyfill0.prototype.catch = function(onRejection) { +PromisePolyfill.prototype.catch = function(onRejection) { return this.then(null, onRejection) } -PromisePolyfill0.resolve = function(value) { - if (value instanceof PromisePolyfill0) return value - return new PromisePolyfill0(function(resolve) {resolve(value)}) +PromisePolyfill.resolve = function(value) { + if (value instanceof PromisePolyfill) return value + return new PromisePolyfill(function(resolve) {resolve(value)}) } -PromisePolyfill0.reject = function(value) { - return new PromisePolyfill0(function(resolve, reject) {reject(value)}) +PromisePolyfill.reject = function(value) { + return new PromisePolyfill(function(resolve, reject) {reject(value)}) } -PromisePolyfill0.all = function(list) { - return new PromisePolyfill0(function(resolve, reject) { +PromisePolyfill.all = function(list) { + return new PromisePolyfill(function(resolve, reject) { var total = list.length, count = 0, values = [] if (list.length === 0) resolve([]) else for (var i = 0; i < list.length; i++) { @@ -167,18 +167,21 @@ PromisePolyfill0.all = function(list) { } }) } -PromisePolyfill0.race = function(list) { - return new PromisePolyfill0(function(resolve, reject) { +PromisePolyfill.race = function(list) { + return new PromisePolyfill(function(resolve, reject) { for (var i = 0; i < list.length; i++) { list[i].then(resolve, reject) } }) } -if (typeof Promise === "undefined") { - if (typeof window !== "undefined") window.Promise = PromisePolyfill0 - else if (typeof global !== "undefined") global.Promise = PromisePolyfill0 +if (typeof window !== "undefined") { + if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill + var PromisePolyfill = window.Promise +} else if (typeof global !== "undefined") { + if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill + var PromisePolyfill = global.Promise +} else { } -var PromisePolyfill = typeof Promise !== "undefined" ? Promise : PromisePolyfill0 var buildQueryString = function(object) { if (Object.prototype.toString.call(object) !== "[object Object]") return "" var args = [] diff --git a/mithril.min.js b/mithril.min.js index a9424e7a..2034f4f6 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,42 @@ -new function(){function w(a,c,g,d,f,l){return{tag:a,key:c,attrs:g,children:d,text:f,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(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,g,d=[],f={};c=P.exec(a);){var l=c[1],k=c[2];""===l&&""!==k?g=k:"#"===l?f.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):f[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(C){throw Error(a);}}function r(a){return a.responseText}function q(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bm.status||304===m.status)c(q(b.type,a));else{var f=Error(m.responseText),g;for(g in a)f[g]=a[g];d(f)}}catch(e){d(e)}};g&&null!=b.data?m.send(b.data):m.send()});return!0===b.background?y:u(y)},jsonp:function(b,k){var r=g();b=d(b,k);var u=new c(function(c, -d){var g=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[g]=function(d){k.parentNode.removeChild(k);c(q(b.type,d));delete a[g]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[g]};null==b.data&&(b.data={});b.url=f(b.url,b.data);b.data[b.callbackKey||"callback"]=g;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?u:r(u)},setCompletionCallback:function(a){u=a}}}(window, -F),O=function(a){function c(e,h,a,b,c,d,f){for(;a=t&&y>=u;){var x=a[t],n=p[u];if(x!==n||h)if(null==x)t++;else if(null==n)u++;else if(x.key===n.key)t++,u++,k(e,x,n,d,q(a,t,f),h,l),h&&x.tag===n.tag&&m(e,r(x),f);else if(x=a[v],x!==n||h)if(null==x)v--;else if(null==n)u++;else if(x.key===n.key)k(e,x,n,d,q(a,v+1,f),h,l),(h||u=t&&y>=u;){x=a[v];n=p[y];if(x!==n||h)if(null==x)v--;else{if(null!=n)if(x.key===n.key)k(e,x,n,d,q(a,v+1,f),h,l),h&&x.tag===n.tag&&m(e,r(x),f),null!=x.dom&&(f=x.dom),v--;else{if(!C){C=a;var x=v,E={},w;for(w=0;wa.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(C){throw Error(a); +}}function r(a){return a.responseText}function q(a,c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bm.status||304===m.status)c(q(b.type,a));else{var f=Error(m.responseText),g;for(g in a)f[g]=a[g];d(f)}}catch(e){d(e)}};g&&null!=b.data?m.send(b.data):m.send()});return!0===b.background?z:u(z)}, +jsonp:function(b,k){var r=g();b=d(b,k);var u=new c(function(c,d){var g=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[g]=function(d){k.parentNode.removeChild(k);c(q(b.type,d));delete a[g]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[g]};null==b.data&&(b.data={});b.url=f(b.url,b.data);b.data[b.callbackKey||"callback"]=g;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0=== +b.background?u:r(u)},setCompletionCallback:function(a){u=a}}}(window,x),M=function(a){function c(e,h,a,b,c,d,f){for(;a=t&&z>=u;){var y=a[t],n=p[u];if(y!==n||h)if(null==y)t++;else if(null==n)u++;else if(y.key===n.key)t++,u++,k(e,y,n,d,q(a,t,f),h,l),h&&y.tag===n.tag&&m(e,r(y),f);else if(y=a[v],y!==n||h)if(null==y)v--;else if(null==n)u++;else if(y.key===n.key)k(e, +y,n,d,q(a,v+1,f),h,l),(h||u=t&&z>=u;){y=a[v];n=p[z];if(y!==n||h)if(null==y)v--;else{if(null!=n)if(y.key===n.key)k(e,y,n,d,q(a,v+1,f),h,l),h&&y.tag===n.tag&&m(e,r(y),f),null!=y.dom&&(f=y.dom),v--;else{if(!C){C=a;var y=v,E={},w;for(w=0;w Date: Thu, 29 Dec 2016 15:58:04 -0500 Subject: [PATCH 30/54] more docs --- docs/framework-comparison.md | 173 +++++++++++++++++++++++++++++++++++ docs/introduction.md | 8 +- docs/vnodes.md | 11 +++ 3 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 docs/framework-comparison.md diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md new file mode 100644 index 00000000..70debb38 --- /dev/null +++ b/docs/framework-comparison.md @@ -0,0 +1,173 @@ +# Framework comparison + +If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril would help you solve your problems more effectively. + +In this page, you will find common arguments about other frameworks and comments on where Mithril is similar or why it differs from them. + +--- + +## React + +React is a view library maintained by Facebook. + +React and Mithril share a lot of similarities + +- They both use virtual DOM, lifecycle methods and key-based reconciliation +- They both organize views via components +- They both use Javascript as a flow control mechanism within views + +The most obvious difference between React and Mithril is in their scope. React is a view library, so a typical React-based application relies on third-party libraries for routing, XHR and state management. Using a library oriented approach allows developers to customize their stack to precisely match their needs. The not-so-nice way of saying that is that too much choice can lead to analysis paralysis, excessive configuration/boilerplate complexity, and [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding). At worst, it can lead to a hodge-podge of different dependencies and architectures, making it difficult for new team members to transfer knowledge from one project to the next. + +Mithril has built-in modules for common necessities such as routing and XHR. This batteries-included approach is preferable for teams that value consistency and ease of onboarding. + +### Performance + +Both React and Mithril care strongly about rendering performance, but go about it in different ways. In the past React had two DOM rendering implementations (one using the DOM API, and one using `innerHTML`). Its upcoming fiber architecture introduces scheduling and prioritization of units of work. React also has a sophisticated build system that disables various checks and error messages for production deployments, and various browser-specific optimizations. In addition, there are also several performance-oriented libraries that leverage React's `shouldComponentUpdate` hook and immutable data structure libraries' fast object equality checking properties to reduce virtual DOM reconciliation times. Generally speaking, React's approach to performance is to engineer relatively complex solutions. + +Mithril follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run. + +Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +55.8 ms | 4.5 ms + +Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. + +Since this is a micro-benchmark, you are encourage to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate. + +For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril's initialization speed is noticeably better than React. + +Here's a slightly more meaningful benchmark: measuring the scripting time for creating 10,000 divs (and 10,000 text nodes). Again, here's the benchmark code for [React](https://jsfiddle.net/bfoeay4f/) and [Mithril](https://jsfiddle.net/fft0ht7n/). Their best results are shown below: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +99.7 ms | 42.8 ms + +What these numbers show is that not only does Mithril initializes significantly faster, it can process upwards of 20,000 virtual DOM nodes before React is ready to use. + +##### Update performance + +Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running. + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Sample results are shown below: + +React v15.4.1 | Mithril 1.0 +------------- | ------- +12.1 ms | 6.4 ms + +##### Development performance + +Another thing to keep in mind is that because React adds extra checks and helpful error messages in development mode, it is slower in development than the production version used for the benchmarks above. To illustrate, [here's the 10,000 node benchmark from above using the development version of React](https://jsfiddle.net/r1jfckrd/). + +### Complexity + +Both React and Mithril have relatively small API surfaces compared to other frameworks, which help ease learning curve. However, whereas idiomatic Mithril can be written without loss of readability using plain ES5 and no other dependencies, idiomatic React relies heavily on complex tooling (e.g. Babel, JSX plugin, etc), and this level of complexity frequently extends to popular parts of its ecosystem, be it in the form of syntax extensions (e.g. non-standard object spread syntax in Redux), architectures (e.g. ones using immutable data libraries), or bells and whistles (e.g. hot module reloading). + +### Learning curve + +Both React and Mithril have relatively small learning curves. React's learning curve mostly involves understanding components and their lifecycle. The learning curve for Mithril components is nearly identical. There are obviously more APIs to learn in Mithril, since Mithril also includes routing and XHR, but the learning curve would be fairly similar to learning React, React Router and a XHR library like superagent or axios. + +Idiomatic React requires working knowledge of JSX and its caveats, and therefore there's also a small learning curve related to Babel. + +### Documentation + +React documentation is clear and well written, and includes a good API reference, tutorials for getting started, as well as pages covering various advanced concepts. + +Mithril documentation also includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. + +--- + +## Angular + +Angular is a web application framework maintained by Google. + +Angular and Mithril are fairly different, but they share a few similarities: + +- Both support componentization +- Both have an array of tools for various aspects of web applications (e.g. routing, XHR) + +The most obvious difference between Angular and Mithril is in their complexity. This can be seen most easily in how views are implemented. Mithril views are plain Javascript, and flow control is done with Javascript built-in mechanisms such as ternary operators or `Array.prototype.map`. Angular, on the other hand, implements a directive system to extend HTML views so that it's possible to evaluate Javascript-like expressions within HTML attributes and interpolations. Angular actually ships with a parser and a compiler written in Javascript to achieve that. If that doesn't seem complex enough, there's actually two compilation modes (a default mode that generates Javascript functions dynamically for performance, and [a slower mode](https://docs.angularjs.org/api/ng/directive/ngCsp) for dealing with Content Security Policy restrictions). + +### Performance + +Angular has made a lot of progress in terms of performance over the years. Angular 1 used a mechanism known as dirty checking which tended to get slow due to the need to constantly diff large `$scope` structures. Angular 2 uses a template change detection mechanism that is much more performant. However, even despite Angular's improvements, Mithril is often faster than Angular, due to the ease of auditing that Mithril's small codebase size affords. + +It's difficult to make a comparison of load times between Angular and Mithril for a couple of reasons. The first is that Angular 1 and 2 are in fact completely different codebases, and both versions are officially supported and maintained (and the vast majority of Angular codebases in the wild currently still use version 1). The second reason is that both Angular and Mithril are modular. In both cases, it's possible to remove a significant part of the framework that is not used in a given application. + +With that being said, the smallest known Angular 2 bundle is a [29kb hello world](https://www.lucidchart.com/techblog/2016/09/26/improving-angular-2-load-times/) compressed w/ the Brotli algorithm (it's 35kb using standard gzip), and with most of Angular's useful functionality removed. By comparison, a Mithril hello world - including the entire Mithril core - would not be over 8kb gzipped (a more optimized bundle could easily be half of that). + +Also, remember that frameworks like Angular and Mithril are designed for non-trivial application, so an application that managed to use all of Angular's API surface would need to download several hundred kb of framework code, rather than merely 29kb. + +##### Update performance + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: + +Angular | Mithril +------- | ------- +11.5 ms | 6.4 ms + +### Complexity + +Angular is superior to Mithril in the amount of tools it offers (in the form of various directives and services), but it is also far more complex. Compare [Angular's API surface](https://angular.io/docs/ts/latest/api/) with [Mithril's](api.md). You can make your own judgment on which API is more self-descriptive and more relevant to your needs. + +Angular 2 has a lot more concepts to understand: on the language level, Typescript is the recommended language, and on top of that there's also Angular-specific template syntax such as bindings, pipes, "safe navigator operator". You also need to learn about architectural concepts such as modules, components, services, directives, etc, and where it's appropriate to use what. + +### Learning curve + +If we compare apples to apples, Angular 2 and Mithril have similar learning curves: in both, components are a central aspect of architecture, and both have reasonable routing and XHR tools. + +With that being said, Angular has a lot more concepts to learn than Mithril. It offers Angular-specific APIs for many things that often can be trivially implemented (e.g. pluralization is essentially a switch statement, "required" validation is simply an equality check, etc). Angular templates also have several layers of abstractions to emulate what Javascript does natively in Mithril - Angular's `ng-if`/`ngIf` is a *directive*, which uses a custom *parser* and *compiler* to evaluate an expression string and emulate lexical scoping... and so on. + +### Documentation + +Angular 2 documentation provides an extensive introductory tutorial, and another tutorial that implements an application. It also has various guides for advanced concepts, a cheatsheet and a style guide. Unfortunately, at the moment, the API reference leaves much to be desired. Several APIs are either undocumented or provide no context for what the API might be used for. + +Mithril documentation includes [introductory](introduction.md) [tutorials](simple-application.md), pages about advanced concepts, and an extensive API reference section, which includes input/output type information, examples for various common use cases and advice against misuse and anti-patterns. It also includes a cheatsheet for quick reference. + +--- + +## Vue + +Vue is a view library similar to Angular. + +Vue and Mithril have a lot of differences but they also share some similarities: + +- They both use virtual DOM and lifecycle methods +- Both organize views via components + +Vue also provides tools for routing and state management as separate modules. Vue looks very similar to Angular and provides a similar directive system, HTML-based templates and logic flow directives. It differs from Angular in that it implements a monkeypatching reactive API that overwrites native methods in a component's data (whereas Angular 1 uses dirty checking and digest/apply cycles to achieve similar results). Similar to Angular 2, Vue compiles HTML templates into functions, but the compiled functions look more like Mithril or React views, rather than Angular's compiled rendering functions. + +Vue is significantly smaller than Angular when comparing apples to apples, but not as small as Mithril (Vue core is around 23kb gzipped, whereas the equivalent rendering module in Mithril is around 4kb gzipped). Both have similar performance characteristics. + +### Performance + +Here's a comparison of library load times, i.e. the time it takes to parse and run the Javascript code for each framework, by adding a `console.time()` call on the first line and a `console.timeEnd()` call on the last of a script that is composed solely of framework code. For your reading convenience, here are best-of-20 results with logging code manually added to bundled scripts, running from the filesystem, in Chrome on a modest 2010 PC desktop: + +Vue | Mithril +------- | ------- +21.8 ms | 4.5 ms + +Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. + +##### Update performance + +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: + +Vue | Mithril +------ | ------- +9.8 ms | 6.4 ms + +### Complexity + +One could argue that Vue templates are more complex than Mithril due to the fact that they use Vue-specific syntax for logic flow, whereas Mithril view language is always just Javascript. + +As of Vue 2.0, it's also possible to write templates using hyperscript/JSX syntax (in addition to single-file components and the various webpack-based language transpilation plugins) so Vue codebases may be less consistent across projects and have higher onboarding costs in terms of technologies compared to idiomatic Mithril projects. + +Vue provides both bi-directional data binding and an optional Redux-like state management library, and unlike Angular, it provides no style guide. The many-ways-of-doing-one-thing approach has a risk of causing architectural fragmentation in long-lived projects. + +The Mithril [tutorial](simple-application.md) implements an *idiomatic* application. This means that while it's *possible* to structure a Mithril codebase in different ways, there's a recommended way to architecture the application. + +### Documentation + +Both Vue and Mithril have thorough documentation. Both include a good API reference with examples, tutorials for getting started, as well as pages covering various advanced concepts. \ No newline at end of file diff --git a/docs/introduction.md b/docs/introduction.md index 86f5d329..2e451a7d 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -12,12 +12,16 @@ ### What is Mithril? -Mithril is a framework for building Single Page Applications. It's small but batteries-included. +Mithril is a client-side Javascript framework for building Single Page Applications. It's small and batteries-included. + +If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. --- ### Getting started +This introduction assumes you know Javacript. If you don't, there are many great resources to learn. [Speaking Javascript](http://speakingjs.com/es5/index.html) is a good e-book for absolute beginners. If you're already familiar with other programming languages, the [Eloquent Javascript](http://eloquentjavascript.net/) e-book might be more suitable for you. [Codecademy](https://www.codecademy.com/learn/javascript) is another good resource that emphasizes learning via interactivity. + The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. Let's create an HTML file to follow along: @@ -86,7 +90,7 @@ m("main", [ ]) ``` -Note: If you prefer `` syntax, [it's possible via Babel](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E). +Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](https://babeljs.io/docs/plugins/transform-react-jsx/). ```markup // HTML syntax via Babel's JSX plugin diff --git a/docs/vnodes.md b/docs/vnodes.md index 9fb23cde..190105a7 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -5,6 +5,7 @@ - [Structure](#structure) - [Vnode types](#vnode-types) - [Monomorphic class](#monomorphic-class) +- [Avoid anti-patterns](#avoid-anti-patterns) --- @@ -100,3 +101,13 @@ Only element tag names and components can be the first argument of the `m()` fun The `mithril/render/vnode` module is used by Mithril to generate all vnodes. This ensures modern Javascript engines can optimize virtual dom diffing by always compiling vnodes to the same hidden class. When creating libraries that emit vnodes, you should use this module instead of writing naked Javascript objects in order to ensure a high level of rendering performance. + +--- + +### Avoid anti-patterns + +#### Avoid memoizing mutable vnodes + +Vnodes are supposed to represent the state of the DOM at a certain point in time. Mithril's rendering engine assumes a reused vnode is unchanged, so modifying a vnode that was used in a previous render will result in undefined behavior. + +It is possible to reuse vnodes to prevent a diff, but it's preferable to use the `onbeforeupdate` hook to make your intent clear to other developers (or your future self). \ No newline at end of file From 531f9a52ba3a5a7c0e639eedcd6857d5826a11cd Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Thu, 29 Dec 2016 15:58:18 -0500 Subject: [PATCH 31/54] rc7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a046b2d1..67e97f8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mithril", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "description": "A framework for building brilliant applications", "author": "Leo Horie", "license": "MIT", From 9d3d6dd6ecb793a589f36c1036682fec09a2295e Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Thu, 29 Dec 2016 21:00:47 +0000 Subject: [PATCH 32/54] Bundled output for commit a2e4cf9a9fad0bee94c1fa79b278013ef090ae6c [skip ci] --- mithril.js | 2 +- mithril.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mithril.js b/mithril.js index f4c77a2e..074077db 100644 --- a/mithril.js +++ b/mithril.js @@ -1151,7 +1151,7 @@ m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0-rc.6" +m.version = "1.0.0-rc.7" m.vnode = Vnode if (typeof module !== "undefined") module["exports"] = m else window.m = m diff --git a/mithril.min.js b/mithril.min.js index 2034f4f6..fe499750 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -38,5 +38,5 @@ l="function"===typeof setImmediate?setImmediate:setTimeout,k,r={prefix:"#!",getP f?(l=k?k.state:null,n=k?k.title:null,a.onpopstate(),k&&k.replace?a.history.replaceState(l,n,r.prefix+c):a.history.pushState(l,n,r.prefix+c)):a.location.href=r.prefix+c},defineRoutes:function(c,k,l){function b(){var b=r.getPath(),f={},g=d(b,f,f),m=a.history.state;if(null!=m)for(var q in m)f[q]=m[q];for(var u in c)if(m=new RegExp("^"+u.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$"),m.test(g)){g.replace(m,function(){for(var a=u.match(/:[^\/]+/g)||[],d=[].slice.call(arguments, 1,-2),g=0;g Date: Fri, 30 Dec 2016 02:52:00 -0500 Subject: [PATCH 33/54] Add support for `options.headers` in `m.request` + tests/docs I also had to edit the mocks accordingly, so I could inspect the headers set. --- docs/request.md | 18 +++++++++++++++-- request/request.js | 8 ++++++-- request/tests/test-request.js | 37 +++++++++++++++++++++++++++++++++++ test-utils/xhrMock.js | 8 +++++++- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/docs/request.md b/docs/request.md index 75be0de9..3671f957 100644 --- a/docs/request.md +++ b/docs/request.md @@ -50,6 +50,7 @@ Argument | Type | Required | Descr `options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network. `options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false` `options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). +`options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`). `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.serialize` | `string = Function(any)` | No | A serialization method to be applied to `data`. Defaults to `JSON.stringify`, or if `options.data` is an instance of [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData), defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function) (i.e. `function(value) {return value}`). `options.deserialize` | `any = Function(string)` | No | A deserialization method to be applied to the response. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses. @@ -404,7 +405,21 @@ function parseCSV(data) { } ``` -Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays +Ignoring the fact that the parseCSV function above doesn't handle a lot of cases that a proper CSV parser would, the code above logs an array of arrays. + +Custom headers may also be helpful in this regard. For example, if you're requesting an SVG, you probably want to set the content type accordingly. To override the default JSON request type, set `options.headers` to an object of key-value pairs corresponding to request header names and values. + +```javascript +m.request({ + method: "GET", + url: "/files/image.svg", + headers: { + "Content-Type": "image/svg+xml; charset=utf-8", + "Accept": "image/svg, text/*" + }, + deserialize: function(value) {return value} +}) +``` --- @@ -485,4 +500,3 @@ m.request("/api/v1/users").then(function(users) { console.log("list of users:", users) }) ``` - diff --git a/request/request.js b/request/request.js index 2a9597ba..11e2e956 100644 --- a/request/request.js +++ b/request/request.js @@ -33,7 +33,7 @@ module.exports = function($window, Promise) { } return args } - + function request(args, extra) { var finalize = finalizer() args = normalize(args, extra) @@ -63,6 +63,10 @@ module.exports = function($window, Promise) { } if (args.withCredentials) xhr.withCredentials = args.withCredentials + for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { + xhr.setRequestHeader(key, args.headers[key]) + } + if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr xhr.onreadystatechange = function() { @@ -93,7 +97,7 @@ module.exports = function($window, Promise) { function jsonp(args, extra) { var finalize = finalizer() args = normalize(args, extra) - + var promise = new Promise(function(resolve, reject) { var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ var script = $window.document.createElement("script") diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 86643e95..a9fce1dd 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -343,6 +343,43 @@ o.spec("xhr", function() { done() }, 20) }) + o("headers are set when header arg passed", function(done) { + mock.$defineRoutes({ + "POST /item": function(request) { + return {status: 200, responseText: ""} + } + }) + xhr({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done) + + function config(xhr) { + o(xhr.getRequestHeader("Custom-Header")).equals("Value") + } + }) + o("headers are with higher precedence than default headers", function(done) { + mock.$defineRoutes({ + "POST /item": function(request) { + return {status: 200, responseText: ""} + } + }) + xhr({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done) + + function config(xhr) { + o(xhr.getRequestHeader("Content-Type")).equals("Value") + } + }) + o("json headers are set to the correct default value", function(done) { + mock.$defineRoutes({ + "POST /item": function(request) { + return {status: 200, responseText: ""} + } + }) + xhr({method: "POST", url: "/item", config: config}).then(done) + + function config(xhr) { + o(xhr.getRequestHeader("Content-Type")).equals("application/json; charset=utf-8") + o(xhr.getRequestHeader("Accept")).equals("application/json, text/*") + } + }) }) o.spec("failure", function() { o("rejects on server error", function(done) { diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index ccf78331..0a7b8f8d 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -14,7 +14,13 @@ module.exports = function() { var $window = { XMLHttpRequest: function XMLHttpRequest() { var args = {} - this.setRequestHeader = function(header, value) {} + var headers = {} + this.setRequestHeader = function(header, value) { + headers[header] = value + } + this.getRequestHeader = function(header) { + return headers[header] + } this.open = function(method, url, async, user, password) { var urlData = parseURL(url, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) args.method = method From 81a540ae410686846b81b249699a8117b014f235 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Fri, 30 Dec 2016 12:21:08 +0000 Subject: [PATCH 34/54] Bundled output for commit ad8f7b6f2ccc90fe8055a318de0ae55abd179838 [skip ci] --- README.md | 2 +- mithril.js | 5 +-- mithril.min.js | 84 +++++++++++++++++++++++++------------------------- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 913fdfe3..b6e99098 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.55 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.59 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 074077db..55810181 100644 --- a/mithril.js +++ b/mithril.js @@ -232,7 +232,6 @@ var _8 = function($window, Promise) { } return args } - function request(args, extra) { var finalize = finalizer() args = normalize(args, extra) @@ -255,6 +254,9 @@ var _8 = function($window, Promise) { xhr.setRequestHeader("Accept", "application/json, text/*") } if (args.withCredentials) xhr.withCredentials = args.withCredentials + for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { + xhr.setRequestHeader(key, args.headers[key]) + } if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr xhr.onreadystatechange = function() { if (xhr.readyState === 4) { @@ -282,7 +284,6 @@ var _8 = function($window, Promise) { function jsonp(args, extra) { var finalize = finalizer() args = normalize(args, extra) - var promise0 = new Promise(function(resolve, reject) { var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ var script = $window.document.createElement("script") diff --git a/mithril.min.js b/mithril.min.js index fe499750..ddf9415f 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,42 +1,42 @@ -new function(){function w(a,c,g,d,f,l){return{tag:a,key:c,attrs:g,children:d,text:f,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(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===G[a]){for(var c,g,d=[],f={};c=N.exec(a);){var l=c[1],k=c[2];""===l&&""!==k?g=k:"#"===l?f.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):f[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(C){throw Error(a); -}}function r(a){return a.responseText}function q(a,c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bm.status||304===m.status)c(q(b.type,a));else{var f=Error(m.responseText),g;for(g in a)f[g]=a[g];d(f)}}catch(e){d(e)}};g&&null!=b.data?m.send(b.data):m.send()});return!0===b.background?z:u(z)}, -jsonp:function(b,k){var r=g();b=d(b,k);var u=new c(function(c,d){var g=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[g]=function(d){k.parentNode.removeChild(k);c(q(b.type,d));delete a[g]};k.onerror=function(){k.parentNode.removeChild(k);d(Error("JSONP request failed"));delete a[g]};null==b.data&&(b.data={});b.url=f(b.url,b.data);b.data[b.callbackKey||"callback"]=g;k.src=l(b.url,b.data);a.document.documentElement.appendChild(k)});return!0=== -b.background?u:r(u)},setCompletionCallback:function(a){u=a}}}(window,x),M=function(a){function c(e,h,a,b,c,d,f){for(;a=t&&z>=u;){var y=a[t],n=p[u];if(y!==n||h)if(null==y)t++;else if(null==n)u++;else if(y.key===n.key)t++,u++,k(e,y,n,d,q(a,t,f),h,l),h&&y.tag===n.tag&&m(e,r(y),f);else if(y=a[v],y!==n||h)if(null==y)v--;else if(null==n)u++;else if(y.key===n.key)k(e, -y,n,d,q(a,v+1,f),h,l),(h||u=t&&z>=u;){y=a[v];n=p[z];if(y!==n||h)if(null==y)v--;else{if(null!=n)if(y.key===n.key)k(e,y,n,d,q(a,v+1,f),h,l),h&&y.tag===n.tag&&m(e,r(y),f),null!=y.dom&&(f=y.dom),v--;else{if(!C){C=a;var y=v,E={},w;for(w=0;wa.indexOf("?")?"?":"&";a+=d+c}return a}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b); +}}function q(b){return b.responseText}function n(b,c){if("function"===typeof b)if(c instanceof Array)for(var a=0;ak.status||304===k.status)c(n(a.type,b));else{var g=Error(k.responseText),e;for(e in b)g[e]=b[e]; +d(g)}}catch(f){d(f)}};h&&null!=a.data?k.send(a.data):k.send()});return!0===a.background?z:t(z)},jsonp:function(a,m){var t=h();a=d(a,m);var q=new c(function(c,d){var h=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,m=b.document.createElement("script");b[h]=function(d){m.parentNode.removeChild(m);c(n(a.type,d));delete b[h]};m.onerror=function(){m.parentNode.removeChild(m);d(Error("JSONP request failed"));delete b[h]};null==a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey|| +"callback"]=h;m.src=l(a.url,a.data);b.document.documentElement.appendChild(m)});return!0===a.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,x),M=function(b){function c(e,f,b,a,c,d,g){for(;b=u&&z>=t;){var y=f[u],p=b[t];if(y!==p||r)if(null==y)u++;else if(null==p)t++;else if(y.key===p.key)u++,t++,m(e,y,p,d,n(f,u,g),r,l),r&&y.tag=== +p.tag&&k(e,q(y),g);else if(y=f[v],y!==p||r)if(null==y)v--;else if(null==p)t++;else if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),(r||t=u&&z>=t;){y=f[v];p=b[z];if(y!==p||r)if(null==y)v--;else{if(null!=p)if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!C){C=f;var y=v,E={},w;for(w=0;w Date: Fri, 30 Dec 2016 07:44:12 -0500 Subject: [PATCH 35/54] recommend webpack as bundling solution --- docs/installation.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 4009bd28..a103c677 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -17,10 +17,11 @@ If you're new to Javascript or just want a very simple setup to get your feet we ```bash # 1) install npm install mithril@rewrite --save +npm install webpack --save # 2) add this line into the scripts section in package.json # "scripts": { -# "build": "bundle index.js --output app.js --watch" +# "build": "webpack index.js app.js --watch" # } # 3) create an `index.js` file @@ -63,16 +64,19 @@ CommonJS is a de-facto standard for modularizing Javascript code, and it's used Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application. -The easiest way to create a bundle is to setup an NPM script for Mithril's bundler. To do so, open the `package.json` that you created earlier, and add an entry to the `scripts` section: +The easiest way to create a bundle is to setup an NPM script for Webpack. To install webpack, run this from the command line: + +```bash +npm install webpack +``` + +Open the `package.json` that you created earlier, and add an entry to the `scripts` section: ``` { "name": "my-project", "scripts": { - "build": "bundle index.js --output app.js --watch" - }, - "dependencies": { - "mithril": "^1.0.0-rc.5" + "build": "webpack index.js app.js --watch" } } ``` @@ -126,25 +130,21 @@ Note that in this example, we're using `m.mount`, which wires up the component t #### Alternate ways to use Mithril -##### Webpack +##### Mithril bundler -Webpack is a popular tool for bundling modular code. The biggest advantage of Webpack is that it has a relatively large ecosystem of plugins. The downside of that is that it can be difficult to configure correctly. +Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. -To use webpack, you must first install it by running `npm install webpack --save-dev`. Then you need to create a `webpack.config.js` file. Here's a basic configuration file that is equivalent to the Mithril bundler command in the first section of this page: +If you want to try it and give feedback, you can open `package.json` and change the npm script for webpack to this: -```javascript -module.exports = { - entry: "./index.js", - output: {filename: "app.js"}, +``` +{ + "name": "my-project", + "scripts": { + "build": "bundle index.js --output app.js --watch" + } } ``` -To run webpack, use the command `webpack --watch`. - -``` -webpack --watch -``` - ##### Vanilla If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all: From 2b1cd9b8f0f8ceb47c4296a4f3c9a52db99f8548 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 30 Dec 2016 07:45:33 -0500 Subject: [PATCH 36/54] add save flag --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index a103c677..569887bc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -67,7 +67,7 @@ Most browser today do not natively support modularization systems (CommonJS or E The easiest way to create a bundle is to setup an NPM script for Webpack. To install webpack, run this from the command line: ```bash -npm install webpack +npm install webpack --save ``` Open the `package.json` that you created earlier, and add an entry to the `scripts` section: From dd81bcd6396984a6cd748fcce02605abdddad939 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 30 Dec 2016 07:48:55 -0500 Subject: [PATCH 37/54] add more docs about global/local installs --- docs/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 569887bc..bee054b8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -83,13 +83,13 @@ Open the `package.json` that you created earlier, and add an entry to the `scrip Remember this is a JSON file, so object key names such as `"scripts"` and `"build"` must be inside of double quotes. -Now you can run the script via `npm run build` in your command line window. This looks up the `bundle` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. +Now you can run the script via `npm run build` in your command line window. This looks up the `webpack` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers. ``` npm run build ``` -The `--watch` flag tells the `bundle` command to watch the file system and automatically recreate `app.js` if file changes are detected. +The `--watch` flag tells webpack to watch the file system and automatically recreate `app.js` if file changes are detected. Now that you have created a bundle, you can then reference the `app.js` file from an HTML file: From ad920483c6567131c17e1ded284a4ebbe778b402 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 30 Dec 2016 07:51:03 -0500 Subject: [PATCH 38/54] reference CDN in vanilla section --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index bee054b8..55cd1346 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -155,7 +155,7 @@ If you don't have the ability to run a bundler script due to company security po Hello world - + From 8746e397b7c26793166ef5c07bf6ba25cf4c80be Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 30 Dec 2016 07:54:01 -0500 Subject: [PATCH 39/54] link mentioned tools --- docs/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 55cd1346..98e6a9ef 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -60,11 +60,11 @@ m.render(document.body, "hello world") Modularization is the practice of separating the code into files. Doing so makes it easier to find code, understand what code relies on what code, and test. -CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like Browserify and Webpack. It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler). +CommonJS is a de-facto standard for modularizing Javascript code, and it's used by Node.js, as well as tools like [Browserify](http://browserify.org/) and [Webpack](https://webpack.js.org/). It's a robust, battle-tested precursor to ES6 modules. Although the syntax for ES6 modules is specified in Ecmascript 6, the actual module loading mechanism is not. If you wish to use ES6 modules despite the non-standardized status of module loading, you can use tools like [Rollup](http://rollupjs.org/), [Babel](https://babeljs.io/) or [Traceur](https://github.com/google/traceur-compiler). Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application. -The easiest way to create a bundle is to setup an NPM script for Webpack. To install webpack, run this from the command line: +The easiest way to create a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: ```bash npm install webpack --save From 3fa771ac2888fef46b2c7829c1ab4b432744e505 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 30 Dec 2016 19:31:51 +0000 Subject: [PATCH 40/54] Test for onbeforeremove delayed resolution --- render/tests/test-onbeforeremove.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 82a859d0..c9af4894 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -158,14 +158,14 @@ o.spec("onbeforeremove", function() { render(root, vnodes) render(root, updated) - + o(root.childNodes.length).equals(2) o(root.firstChild.firstChild.nodeValue).equals("1") - + callAsync(function() { o(root.childNodes.length).equals(1) o(root.firstChild.firstChild.nodeValue).equals("2") - + done() }) }) @@ -184,4 +184,25 @@ o.spec("onbeforeremove", function() { done() }) }) + o("awaits promise resolution before removing the node", function(done) { + var view = o.spy() + var onremove = o.spy() + var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} + var component = { + onbeforeremove: onbeforeremove, + onremove: onremove, + view: view, + } + render(root, [{tag: component}]) + render(root, []) + + callAsync(function(){ + o(onremove.callCount).equals(0) + + callAsync(function() { + o(onremove.callCount).equals(1) + done() + }) + }) + }) }) From b14ca82c2ead1dac7d50ff472d254d6d430c62ca Mon Sep 17 00:00:00 2001 From: Dmitri Zaitsev Date: Sat, 31 Dec 2016 03:32:57 +0000 Subject: [PATCH 41/54] Recommend 'budo' for the quickest start ever Fewer steps to get a complete app running in the browser with live reload. --- docs/installation.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 98e6a9ef..24135b10 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -12,7 +12,34 @@ If you're new to Javascript or just want a very simple setup to get your feet we ### NPM -#### Quick start +#### Quick start with Budo + +The [budo browserify development server](https://github.com/mattdesl/budo) +allows for the fastest way to have your pure JavaScript app (no HTML file needed) +running in the browser with the covenient live reload feature on any source changes. + +```bash +# 1) install +npm install mithril@rewrite --save +npm install budo -g + +# 2) add this line into the scripts section in package.json +# "scripts": { +# "start": "budo --live --open index.js" +# } + +# 3) create an `index.js` file + +# 4) run budo +npm start +``` + +The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. +Any changes in the source files will instantly get recompiled and the +browser will refresh reflecting the changes. + + +#### Quick start with Webpack ```bash # 1) install @@ -26,8 +53,13 @@ npm install webpack --save # 3) create an `index.js` file -# 4) run bundler +# 4) create an `index.html` file loading `app.js` + +# 5) run bundler npm run build + +# 6) open `index.html` in the (default) browser +open index.html ``` #### Step by step @@ -43,7 +75,13 @@ npm init --yes # creates a file called package.json ``` -Then, run `npm install mithril@rewrite --save` to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file +Then, run + +```bash +npm install mithril@rewrite --save +``` + +to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file ```bash npm install mithril@rewrite --save From c71ebf18edf8e405941a84a6aa459f155cf4114d Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 31 Dec 2016 08:35:50 -0500 Subject: [PATCH 42/54] tweaks in docs --- docs/contributing.md | 2 +- docs/fragment.md | 17 ++++++++++ docs/framework-comparison.md | 4 +++ docs/guides.md | 3 +- docs/installation.md | 62 +++++++++++++++++++----------------- docs/style.css | 7 ++-- docs/testing.md | 4 --- 7 files changed, 61 insertions(+), 38 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index e2e65fd6..5b4770d3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,7 +4,7 @@ ## How do I go about contributing ideas or new features? -Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. And don't worry, we're nice :) +Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities. diff --git a/docs/fragment.md b/docs/fragment.md index cd1ff53a..018dc4ab 100644 --- a/docs/fragment.md +++ b/docs/fragment.md @@ -10,6 +10,23 @@ Allows attaching lifecycle methods to a fragment [vnode](vnodes.md) +```javascript +var groupVisible = true +var log = function() { + console.log("group is now visible") +} + +m("ul", [ + m("li", "child 1"), + m("li", "child 2"), + groupVisible ? m.fragment({oninit: log}, [ + // a fragment containing two elements + m("li", "child 3"), + m("li", "child 4"), + ]) : null +]) +``` + --- ### Signature diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md index 70debb38..029d0319 100644 --- a/docs/framework-comparison.md +++ b/docs/framework-comparison.md @@ -1,5 +1,9 @@ # Framework comparison +- [React](#react) +- [Angular](#angular) +- [Vue](#vue) + If you're reading this page, you probably have used other frameworks to build applications, and you want to know if Mithril would help you solve your problems more effectively. In this page, you will find common arguments about other frameworks and comments on where Mithril is similar or why it differs from them. diff --git a/docs/guides.md b/docs/guides.md index 4abbf045..a4333b58 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -1,7 +1,7 @@ - Tutorials - [Installation](installation.md) - [Introduction](introduction.md) - - [Tutorial](tutorial.md) + - [Tutorial](simple-application.md) - [Testing](testing.md) - [Examples](examples.md) - Key concepts @@ -14,4 +14,5 @@ - [Contributing](contributing.md) - [Credits](credits.md) - Misc + - [Framework comparison](framework-comparison.md) - [Change log/Migration](change-log.md) diff --git a/docs/installation.md b/docs/installation.md index 24135b10..7536cbce 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,5 +1,8 @@ # Installation +- [CDN](#cdn) +- [NPM](#npm) + ### CDN If you're new to Javascript or just want a very simple setup to get your feet wet, you can get Mithril from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network): @@ -12,33 +15,6 @@ If you're new to Javascript or just want a very simple setup to get your feet we ### NPM -#### Quick start with Budo - -The [budo browserify development server](https://github.com/mattdesl/budo) -allows for the fastest way to have your pure JavaScript app (no HTML file needed) -running in the browser with the covenient live reload feature on any source changes. - -```bash -# 1) install -npm install mithril@rewrite --save -npm install budo -g - -# 2) add this line into the scripts section in package.json -# "scripts": { -# "start": "budo --live --open index.js" -# } - -# 3) create an `index.js` file - -# 4) run budo -npm start -``` - -The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. -Any changes in the source files will instantly get recompiled and the -browser will refresh reflecting the changes. - - #### Quick start with Webpack ```bash @@ -62,6 +38,10 @@ npm run build open index.html ``` +The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. +Any changes in the source files will instantly get recompiled and the +browser will refresh reflecting the changes. + #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. @@ -166,9 +146,31 @@ m.mount(document.body, MyComponent) Note that in this example, we're using `m.mount`, which wires up the component to Mithril's autoredraw system. In most applications, you will want to use `m.mount` (or `m.route` if your application has multiple screens) instead of `m.render` to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs. -#### Alternate ways to use Mithril +--- -##### Mithril bundler +### Alternate ways to use Mithril + +#### Live reload development environment + +Live reload is a feature where code changes automatically trigger the page to reload. [Budo](https://github.com/mattdesl/budo) is one tool that enables live reloading. + +```bash +# 1) install +npm install mithril@rewrite --save +npm install budo -g + +# 2) add this line into the scripts section in package.json +# "scripts": { +# "start": "budo --live --open index.js" +# } + +# 3) create an `index.js` file + +# 4) run budo +npm start +``` + +#### Mithril bundler Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. @@ -183,7 +185,7 @@ If you want to try it and give feedback, you can open `package.json` and change } ``` -##### Vanilla +#### Vanilla If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all: diff --git a/docs/style.css b/docs/style.css index faef030c..d80b9bca 100644 --- a/docs/style.css +++ b/docs/style.css @@ -5,8 +5,11 @@ nav a {border-left:1px solid #ddd;padding:0 10px;} nav a:first-child {border:0;padding-left:0;} main {margin-bottom:100px;} main section {margin-left:270px;} -h1 {margin:0 0 15px;} -h5 {font-style:italic;} +h1 {font-size:24px;margin:0 0 15px;} +h2 {font-size:22px;margin:30px 0 15px;} +h3 {font-size:20px;margin:30px 0 15px;} +h4 {font-size:18px;margin:15px 0 15px;} +h5 {font-weight:bold;margin:15px 0 15px;} pre,code {background:#eee;font-family:monospace;} pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;} code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:3px;white-space:pre;} diff --git a/docs/testing.md b/docs/testing.md index 63f231cf..ad4aaba0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -8,11 +8,7 @@ The easist way to setup the test runner is to create an NPM script for it. Open { "name": "my-project", "scripts": { - "build": "bundle index.js --output app.js --watch", "test": "ospec" - }, - "dependencies": { - "mithril": "^1.0.0-rc.5" } } ``` From afcf28ab199df5e137069c48de087e5c80bd77fa Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 31 Dec 2016 09:38:35 -0500 Subject: [PATCH 43/54] more documentation --- docs/guides.md | 2 ++ docs/introduction.md | 7 ++++--- docs/testing.md | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/guides.md b/docs/guides.md index a4333b58..3b3c537c 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -11,6 +11,8 @@ - [Keys](keys.md) - Social - [Community chat](https://gitter.im/lhorie/mithril.js) + - [Wiki](https://github.com/lhorie/mithril.js/wiki) + - [Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) - [Contributing](contributing.md) - [Credits](credits.md) - Misc diff --git a/docs/introduction.md b/docs/introduction.md index 2e451a7d..9a59f3b8 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -12,15 +12,16 @@ ### What is Mithril? -Mithril is a client-side Javascript framework for building Single Page Applications. It's small and batteries-included. +Mithril is a client-side Javascript framework for building Single Page Applications. +It's small (< 8kb gzip) and batteries-included. If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. --- -### Getting started +Note: This introduction assumes you have basic level of Javacript knowledge. If you don't, there are many great resources to learn. [Speaking Javascript](http://speakingjs.com/es5/index.html) is a good e-book for absolute beginners. If you're already familiar with other programming languages, the [Eloquent Javascript](http://eloquentjavascript.net/) e-book might be more suitable for you. [Codecademy](https://www.codecademy.com/learn/javascript) is another good resource that emphasizes learning via interactivity. -This introduction assumes you know Javacript. If you don't, there are many great resources to learn. [Speaking Javascript](http://speakingjs.com/es5/index.html) is a good e-book for absolute beginners. If you're already familiar with other programming languages, the [Eloquent Javascript](http://eloquentjavascript.net/) e-book might be more suitable for you. [Codecademy](https://www.codecademy.com/learn/javascript) is another good resource that emphasizes learning via interactivity. +### Getting started The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. diff --git a/docs/testing.md b/docs/testing.md index ad4aaba0..b2615649 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -44,3 +44,42 @@ Writing tests upfront requires specifications to be frozen. Upfront tests are a Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertedly broken and that previously fixed bugs do not get re-introduced by unrelated changes. +--- + +### Unit testing + +Unit testing is the practice of isolating a part of an application (typically a single module), and asserting that, given some inputs, it produces the expected outputs. + +Testing a Mithril component is easy. Let's assume we have a simple component like this: + +```javascript +// MyComponent.js +var m = require("mithril") + +module.exports = { + view: function() { + return m("div", "Hello world") + } +} +``` + +We can then create a `tests/MyComponent.js` file and create a test for this component like this: + +``` +var MyComponent = require("MyComponent") + +o.spec("MyComponent", function() { + o("returns a div", function() { + var vnode = MyComponent.view() + + o(vnode.tag).equals("div") + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("Hello world") + }) +}) +``` + +Typically, you wouldn't test the structure of the vnode tree so granularly, and you would instead only test non-trivial, dynamic aspects of the view. A tool that can help making testing easier with deep vnode trees is [Mithril Query](https://github.com/StephanHoyer/mithril-query). + +Sometimes, you need to mock the dependencies of a module in order to test the module in isolation. [Mockery](https://github.com/mfncooper/mockery) is one tool that allows you to do that. From d7732d35608141554217834929acbe4f3c858e88 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 31 Dec 2016 09:41:30 -0500 Subject: [PATCH 44/54] syntax highlighting --- docs/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index b2615649..e42696af 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -65,7 +65,7 @@ module.exports = { We can then create a `tests/MyComponent.js` file and create a test for this component like this: -``` +```javascript var MyComponent = require("MyComponent") o.spec("MyComponent", function() { From b62663bdc15fa7cfbe594634d3509da46b85f5b1 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 31 Dec 2016 19:27:23 -0500 Subject: [PATCH 45/54] ensure properties in arrays are tested #1497 --- ospec/README.md | 2 +- ospec/ospec.js | 7 +++++-- ospec/package.json | 2 +- ospec/tests/test-ospec.js | 10 +++++++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ospec/README.md b/ospec/README.md index cd08c697..084d949d 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -4,7 +4,7 @@ Noiseless testing framework -Version: 1.2.1 +Version: 1.2.2 License: MIT ## About diff --git a/ospec/ospec.js b/ospec/ospec.js index f094dfe6..24495b87 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -143,8 +143,11 @@ module.exports = new function init() { return true } if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) { - for (var i = 0; i < a.length; i++) { - if (!deepEqual(a[i], b[i])) return false + var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b) + if (aKeys.length !== bKeys.length) return false + var larger = aKeys.length < bKeys.length ? bKeys : aKeys + for (var i = 0; i < larger.length; i++) { + if (!deepEqual(a[larger[i]], b[larger[i]])) return false } return true } diff --git a/ospec/package.json b/ospec/package.json index 940b7f5a..1127ad7f 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -1,6 +1,6 @@ { "name": "ospec", - "version": "1.2.1", + "version": "1.2.2", "description": "Noiseless testing framework", "main": "ospec.js", "directories": { diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 3b1dd440..a1433d26 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -28,7 +28,7 @@ o.spec("ospec", function() { o.beforeEach(function() {b = 1}) o.afterEach(function() {b = 0}) - o("assertions", function() { + o.only("assertions", function() { var spy = o.spy() spy(a) @@ -36,6 +36,14 @@ o.spec("ospec", function() { o(a).notEquals(2) o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3}) o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}]) + + var monkeypatch1 = [1, 2] + monkeypatch1.field = 3 + var monkeypatch2 = [1, 2] + monkeypatch2.field = 4 + + o(monkeypatch1).notDeepEquals([1, 2]) + o(monkeypatch1).notDeepEquals(monkeypatch2) var values = ["a", "", 1, 0, true, false, null, undefined, Date(0), ["a"], [], function() {return arguments}.call(), new Uint8Array(), {a: 1}, {}] for (var i = 0; i < values.length; i++) { From fab51f583a2b44843a11cbdf0f498ebdb13eef00 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 31 Dec 2016 19:27:53 -0500 Subject: [PATCH 46/54] clean up docs --- docs/guides.md | 5 ++--- docs/introduction.md | 2 +- docs/methods.md | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/guides.md b/docs/guides.md index 3b3c537c..d2bd87da 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -11,9 +11,8 @@ - [Keys](keys.md) - Social - [Community chat](https://gitter.im/lhorie/mithril.js) - - [Wiki](https://github.com/lhorie/mithril.js/wiki) - - [Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) - - [Contributing](contributing.md) + - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) + - [How to contribute](contributing.md) - [Credits](credits.md) - Misc - [Framework comparison](framework-comparison.md) diff --git a/docs/introduction.md b/docs/introduction.md index 9a59f3b8..29c59a4e 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -13,7 +13,7 @@ ### What is Mithril? Mithril is a client-side Javascript framework for building Single Page Applications. -It's small (< 8kb gzip) and batteries-included. +It's small (< 8kb gzip), fast and batteries-included. If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. diff --git a/docs/methods.md b/docs/methods.md index 9b19b4cc..06809b50 100644 --- a/docs/methods.md +++ b/docs/methods.md @@ -16,5 +16,4 @@ - Optional - [Stream](stream.md) - Tooling - - [Bundler](bundler.md) - - [Ospec](ospec.md) + - [Ospec](https://github.com/lhorie/mithril.js/blob/rewrite/ospec) From af39796da3d27c92a90499c487348895f08f45d5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 1 Jan 2017 10:59:40 +0100 Subject: [PATCH 47/54] [ospec:tests] Add assertions for array deepEquals corner cases --- ospec/tests/test-ospec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index a1433d26..8896c4a0 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -37,6 +37,13 @@ o.spec("ospec", function() { o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3}) o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}]) + var sparse1 = [void 1, void 2, void 3] + delete sparse1[0] + var sparse2 = [void 1, void 2, void 3] + delete sparse2[1] + + o(sparse1).notDeepEquals(sparse2) + var monkeypatch1 = [1, 2] monkeypatch1.field = 3 var monkeypatch2 = [1, 2] @@ -45,6 +52,14 @@ o.spec("ospec", function() { o(monkeypatch1).notDeepEquals([1, 2]) o(monkeypatch1).notDeepEquals(monkeypatch2) + monkeypatch2.field = 3 + o(monkeypatch1).deepEquals(monkeypatch2) + + monkeypatch1.undef = undefined + monkeypatch2.UNDEF = undefined + + o(monkeypatch1).notDeepEquals(monkeypatch2) + var values = ["a", "", 1, 0, true, false, null, undefined, Date(0), ["a"], [], function() {return arguments}.call(), new Uint8Array(), {a: 1}, {}] for (var i = 0; i < values.length; i++) { for (var j = 0; j < values.length; j++) { From 02545a8a98eefd6d9867ec7bd0c006c1ee8cbe2b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 1 Jan 2017 15:08:46 +0100 Subject: [PATCH 48/54] [ospec] Fix array deepEquals corner cases --- ospec/ospec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 24495b87..9db636b4 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -145,9 +145,8 @@ module.exports = new function init() { if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) { var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b) if (aKeys.length !== bKeys.length) return false - var larger = aKeys.length < bKeys.length ? bKeys : aKeys - for (var i = 0; i < larger.length; i++) { - if (!deepEqual(a[larger[i]], b[larger[i]])) return false + for (var i = 0; i < aKeys.length; i++) { + if (!b.hasOwnProperty(aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false } return true } From 027a2207799efd2ff41f7b83a82e16101d447507 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 1 Jan 2017 16:09:15 +0100 Subject: [PATCH 49/54] [ospec:tests] Add assertion for object with undefined properties vs none --- ospec/tests/test-ospec.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 8896c4a0..b3878f7a 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -36,6 +36,13 @@ o.spec("ospec", function() { o(a).notEquals(2) o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3}) o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}]) + + var undef1 = {undef: void 0} + var undef2 = {UNDEF: void 0} + + o(undef1).notDeepEquals(undef2) + o(undef1).notDeepEquals({}) + o({}).notDeepEquals(undef1) var sparse1 = [void 1, void 2, void 3] delete sparse1[0] From df1e19b86cd00b3504796cab1c138230e7734e1b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 1 Jan 2017 16:13:11 +0100 Subject: [PATCH 50/54] [ospec] Fix for objects with undefined properties --- ospec/ospec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 9db636b4..56111b66 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -135,7 +135,7 @@ module.exports = new function init() { var aIsArgs = isArguments(a), bIsArgs = isArguments(b) if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) { for (var i in a) { - if (!deepEqual(a[i], b[i])) return false + if ((!(i in b)) || !deepEqual(a[i], b[i])) return false } for (var i in b) { if (!(i in a)) return false From ba96c23b84e52f3c96fd717e28c8b816cb5a38a7 Mon Sep 17 00:00:00 2001 From: Hugo Freitas Date: Mon, 2 Jan 2017 17:30:49 -0300 Subject: [PATCH 51/54] Remove duplicated install --- docs/installation.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 7536cbce..2d20dd11 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,10 +63,6 @@ npm install mithril@rewrite --save to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file -```bash -npm install mithril@rewrite --save -``` - You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules: ```javascript From 933266670d35ec5c4b683ec428272c231b6e022b Mon Sep 17 00:00:00 2001 From: bruce-one Date: Tue, 3 Jan 2017 10:49:30 +1100 Subject: [PATCH 52/54] Recommend usage of fragments --- docs/keys.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/keys.md b/docs/keys.md index 245e078b..9fd87814 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -132,9 +132,7 @@ users.map(function(u) { // PREFER users.map(function(u) { - return {tag: "[", key: u.id, children: [ - m("button", u.name) - ]} + return m.fragment({key: u.id}, m("button", u.name)) }) ``` From 7868d9700654df1ddf9839f208bd642f0caba4a3 Mon Sep 17 00:00:00 2001 From: Bryce Gibson Date: Tue, 3 Jan 2017 11:19:55 +1100 Subject: [PATCH 53/54] Remove o.only call to ensure other tests are run. --- ospec/tests/test-ospec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index b3878f7a..2bdd5556 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -8,7 +8,7 @@ new function(o) { o.spec("ospec", function() { o("skipped", function() { - o(1).equals(1) + o(true).equals(false) }) o.only(".only()", function() { o(2).equals(2) @@ -28,7 +28,7 @@ o.spec("ospec", function() { o.beforeEach(function() {b = 1}) o.afterEach(function() {b = 0}) - o.only("assertions", function() { + o("assertions", function() { var spy = o.spy() spy(a) From 33fb63a72ba585fdf51bdd319e4a9a68829b1bc1 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 3 Jan 2017 10:47:35 -0500 Subject: [PATCH 54/54] fix merge --- docs/installation.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 2d20dd11..6993c5d5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,10 +38,6 @@ npm run build open index.html ``` -The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. -Any changes in the source files will instantly get recompiled and the -browser will refresh reflecting the changes. - #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. @@ -166,6 +162,8 @@ npm install budo -g npm start ``` +The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes. + #### Mithril bundler Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet.