From 1dd5fe310127557ad19c679f9115a05981e0518e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 19 Feb 2017 22:53:46 +0100 Subject: [PATCH 01/12] Tests: Some more render/tests/test-component.js refactoring --- render/tests/test-component.js | 307 ++++++++++++++++++--------------- 1 file changed, 171 insertions(+), 136 deletions(-) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 64cd6fcb..6398eaa0 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -656,6 +656,130 @@ o.spec("component", function() { o(vnode.dom).notEquals(updated.dom) }) + o("lifecycle timing megatest (for a single component)", function() { + var methods = { + view: o.spy(function() { + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + // the `attrs` hooks are called before the component ones + attrs[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount + 1) + }) + methods[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount) + }) + }) + + var component = createComponent(methods) + + o(methods.view.callCount).equals(0) + o(methods.oninit.callCount).equals(0) + o(methods.oncreate.callCount).equals(0) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(1) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, []) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(1) + o(methods.onremove.callCount).equals(1) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + }) + o("hook state and arguments validation", function(){ + var methods = { + view: o.spy(function(vnode) { + o(this).equals(vnode.state) + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + attrs[hook] = o.spy(function(vnode){ + // #1638, the assertion passes for `oninit` because both values are wrong. + if (hook !== 'oncreate') o(this).equals(vnode.state)(hook) + }) + methods[hook] = o.spy(function(vnode){ + o(this).equals(vnode.state) + }) + }) + + var component = createComponent(methods) + + render(root, [{tag: component, attrs: attrs}]) + render(root, [{tag: component, attrs: attrs}]) + render(root, []) + + hooks.forEach(function(hook) { + // #1638 + if (hook !== "oninit" && hook !== 'oncreate') o(attrs[hook].this).equals(methods.view.this)(hook) + o(methods[hook].this).equals(methods.view.this)(hook) + }) + + o(methods.view.args.length).equals(1) + o(methods.oninit.args.length).equals(1) + o(methods.oncreate.args.length).equals(1) + o(methods.onbeforeupdate.args.length).equals(2) + o(methods.onupdate.args.length).equals(1) + o(methods.onbeforeremove.args.length).equals(1) + o(methods.onremove.args.length).equals(1) + + hooks.forEach(function(hook) { + o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) + }) + + }) }) o.spec("state", function() { o("initializes state", function() { @@ -675,7 +799,7 @@ o.spec("component", function() { o(vnode.state.data).equals(data) } }) - o('state "copy" is shallow', function() { + o('state proxies to the component object/prototype', function() { var called = 0 var body = {a: 1} var data = [body] @@ -698,11 +822,10 @@ o.spec("component", function() { }) }) o.spec("Tests specific to certain component kinds", function() { - - o.spec("POJO state", function() { - o("copies state", function() { + o.spec("state", function() { + o("POJO", function() { var called = 0 - var data = {a: 1} + var data = {} var component = { data: data, oninit: init, @@ -721,142 +844,54 @@ o.spec("component", function() { o(vnode.state.x).equals(1) } }) - }) + o("Constructible", function() { + var oninit = o.spy() + var component = o.spy(function(vnode){ + o(vnode.state).equals(null) + // #1638 + // o(oninit.callCount).equals(0) + }) + var view = o.spy(function(){ + o(this instanceof component).equals(true) + return "" + }) + component.prototype.view = view + component.prototype.oninit = oninit - o("Classes can be used as components", function() { - function MyComponent(vnode){ - o(vnode.state).equals(null) - } - var proto = MyComponent.prototype + var context - var context + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) - proto.oninit = o.spy(function(vnode) { - o(this).equals(vnode.state) - context = this + o(component.callCount).equals(1) + o(oninit.callCount).equals(2) + o(view.callCount).equals(2) }) - proto.oncreate = o.spy() - proto.onbeforeupdate = o.spy() - proto.onupdate = o.spy() - proto.onbeforeremove = o.spy() - proto.onremove = o.spy() - proto.view = o.spy(function() { - return "" + o("Closure", function() { + var state + var oninit = o.spy() + var view = o.spy(function() { + o(this).equals(state) + return "" + }) + var component = o.spy(function(vnode) { + o(vnode.state).equals(null) + // #1638 + // o(oninit.callCount).equals(0) + return state = { + view: view + } + }) + + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) + + o(component.callCount).equals(1) + o(oninit.callCount).equals(1) + o(view.callCount).equals(2) }) - - render(root, [{tag: MyComponent}]) - - o(context instanceof MyComponent).equals(true) - - o(proto.view.callCount).equals(1) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(0) - o(proto.onupdate.callCount).equals(0) - o(proto.onbeforeremove.callCount).equals(0) - o(proto.onremove.callCount).equals(0) - - render(root, [{tag: MyComponent}]) - - o(proto.view.callCount).equals(2) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(1) - o(proto.onupdate.callCount).equals(1) - o(proto.onbeforeremove.callCount).equals(0) - o(proto.onremove.callCount).equals(0) - - render(root, []) - - o(proto.view.callCount).equals(2) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(1) - o(proto.onupdate.callCount).equals(1) - o(proto.onbeforeremove.callCount).equals(1) - o(proto.onremove.callCount).equals(1) - - o(proto.oninit.this).equals(context) - o(proto.view.this).equals(context) - o(proto.oncreate.this).equals(context) - o(proto.onbeforeupdate.this).equals(context) - o(proto.onupdate.this).equals(context) - o(proto.onbeforeremove.this).equals(context) - o(proto.onremove.this).equals(context) - - o(proto.oninit.args.length).equals(1) - o(proto.view.args.length).equals(1) - o(proto.oncreate.args.length).equals(1) - o(proto.onbeforeupdate.args.length).equals(2) - o(proto.onupdate.args.length).equals(1) - o(proto.onbeforeremove.args.length).equals(1) - o(proto.onremove.args.length).equals(1) - }) - o("Closure functions can be used as components", function() { - var state, context - function component(vnode) { - o(vnode.state).equals(null) - - return state = { - oninit: o.spy(function(vnode) { - o(this).equals(vnode.state) - context = this - }), - oncreate: o.spy(), - onbeforeupdate: o.spy(), - onupdate: o.spy(), - onbeforeremove: o.spy(), - onremove: o.spy(), - view: o.spy(function() { - return "" - }) - } - } - - render(root, [{tag: component}]) - - o(state).equals(context) - - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(1) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(0) - o(state.onupdate.callCount).equals(0) - o(state.onbeforeremove.callCount).equals(0) - o(state.onremove.callCount).equals(0) - - render(root, [{tag: component}]) - - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(2) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(1) - o(state.onupdate.callCount).equals(1) - o(state.onbeforeremove.callCount).equals(0) - o(state.onremove.callCount).equals(0) - - render(root, []) - - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(2) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(1) - o(state.onupdate.callCount).equals(1) - o(state.onbeforeremove.callCount).equals(1) - o(state.onremove.callCount).equals(1) - - o(state.oninit.this).equals(state) - o(state.view.this).equals(state) - o(state.oncreate.this).equals(state) - o(state.onbeforeupdate.this).equals(state) - o(state.onupdate.this).equals(state) - o(state.onbeforeremove.this).equals(state) - o(state.onremove.this).equals(state) - - o(state.oninit.args.length).equals(1) - o(state.view.args.length).equals(1) - o(state.oncreate.args.length).equals(1) - o(state.onbeforeupdate.args.length).equals(2) - o(state.onupdate.args.length).equals(1) - o(state.onbeforeremove.args.length).equals(1) - o(state.onremove.args.length).equals(1) }) }) }) From fc038f9d859e5e458fb50938f7ef446337d5d620 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 19 Feb 2017 22:57:31 +0100 Subject: [PATCH 02/12] Tests: enable tests for #1638 --- render/tests/test-component.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 6398eaa0..212905d2 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -747,8 +747,7 @@ o.spec("component", function() { ] hooks.forEach(function(hook) { attrs[hook] = o.spy(function(vnode){ - // #1638, the assertion passes for `oninit` because both values are wrong. - if (hook !== 'oncreate') o(this).equals(vnode.state)(hook) + o(this).equals(vnode.state)(hook) }) methods[hook] = o.spy(function(vnode){ o(this).equals(vnode.state) @@ -762,8 +761,7 @@ o.spec("component", function() { render(root, []) hooks.forEach(function(hook) { - // #1638 - if (hook !== "oninit" && hook !== 'oncreate') o(attrs[hook].this).equals(methods.view.this)(hook) + o(attrs[hook].this).equals(methods.view.this)(hook) o(methods[hook].this).equals(methods.view.this)(hook) }) @@ -848,8 +846,7 @@ o.spec("component", function() { var oninit = o.spy() var component = o.spy(function(vnode){ o(vnode.state).equals(null) - // #1638 - // o(oninit.callCount).equals(0) + o(oninit.callCount).equals(0) }) var view = o.spy(function(){ o(this instanceof component).equals(true) @@ -877,8 +874,7 @@ o.spec("component", function() { }) var component = o.spy(function(vnode) { o(vnode.state).equals(null) - // #1638 - // o(oninit.callCount).equals(0) + o(oninit.callCount).equals(0) return state = { view: view } From 7668ddd120c00f4152cdaf5591043e5ce6bff70e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 19 Feb 2017 23:00:34 +0100 Subject: [PATCH 03/12] fix #1638 --- render/render.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/render/render.js b/render/render.js index ea015ebd..eb31229b 100644 --- a/render/render.js +++ b/render/render.js @@ -20,8 +20,8 @@ module.exports = function($window) { } function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": return createText(parent, vnode, nextSibling) case "<": return createHTML(parent, vnode, nextSibling) @@ -116,6 +116,7 @@ module.exports = function($window) { sentinel.$$reentrantLock$$ = true } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null From e496f7bfa6e222fa07fea84effa1c55ddf127069 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 20 Feb 2017 16:46:34 +0100 Subject: [PATCH 04/12] Test: ensure that recycled components get a fresh state --- render/tests/test-component.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 212905d2..0ff4e07e 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -776,7 +776,28 @@ o.spec("component", function() { hooks.forEach(function(hook) { o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) }) + }) + o("recycled components get a fresh state", function() { + var step = 0 + var firstState + var view = o.spy(function(vnode) { + if (step === 0) { + firstState = vnode.state + } else { + o(vnode.state).notEquals(firstState) + } + return {tag: 'div'} + }) + var component = createComponent({view: view}) + render(root, [{tag: 'div', children: [{tag: component, key: 1}]}]) + var child = root.firstChild.firstChild + render(root, []) + step = 1 + render(root, [{tag: 'div', children: [{tag: component, key: 1}]}]) + + o(child).equals(root.firstChild.firstChild) + o(view.callCount).equals(2) }) }) o.spec("state", function() { From 6a7144fc89b63944110ca4ce3169adcbbfee34a0 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 20 Feb 2017 20:29:18 +0100 Subject: [PATCH 05/12] Tests: add a test for onbeforeupdate and recycled nodes --- render/tests/test-onbeforeupdate.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index cf16dd1c..93643409 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -120,6 +120,21 @@ o.spec("onbeforeupdate", function() { o(count).equals(1) }) + o("doesn't fire on recycled nodes", function() { + var onbeforeupdate = o.spy() + var vnodes = [{tag: "div", key: 1}] + var temp = [] + var updated = [{tag: "div", key: 1, attrs: {onbeforeupdate: onbeforeupdate}}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(vnodes[0].dom).equals(updated[0].dom) + o(updated[0].dom.nodeName).equals("DIV") + o(onbeforeupdate.callCount).equals(0) + }) + components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create From 3e7649ef06faa7dbdb7ca69a61dada993e8b70ee Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Fri, 24 Feb 2017 09:26:26 +0100 Subject: [PATCH 06/12] Fix recycled components initialization fix #1641 --- render/render.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/render/render.js b/render/render.js index eb31229b..19ca6ebe 100644 --- a/render/render.js +++ b/render/render.js @@ -100,7 +100,7 @@ module.exports = function($window) { } return element } - function createComponent(parent, vnode, hooks, ns, nextSibling) { + function initComponent(vnode, hooks) { var sentinel if (typeof vnode.tag === "function") { vnode.state = null @@ -120,7 +120,9 @@ module.exports = function($window) { initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null - + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -233,11 +235,12 @@ module.exports = function($window) { if (oldTag === tag) { vnode.state = old.state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) initLifecycle(vnode.attrs, vnode, hooks) + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break @@ -307,8 +310,13 @@ module.exports = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) - updateLifecycle(vnode.state, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) @@ -562,11 +570,10 @@ module.exports = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old) From 71dcde6adf05c090c50a8ea1af47e37e4d147cd9 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 24 Feb 2017 10:54:07 +0000 Subject: [PATCH 07/12] Warn about data models w key props passed as attrs Hint to avoid problems such as those found here [February 23, 2017 6:28 PM](https://gitter.im/lhorie/mithril.js?at=58af29c47ceae5376a2d470e) --- docs/keys.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/keys.md b/docs/keys.md index 2c1e46d7..cfca7cdb 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -177,3 +177,29 @@ m("div", [ ] ]) ``` + +#### Avoid passing model data directly to components if the model uses `key` as a data property + +The `key` property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose `key` property is expected to change over time. This can lead to components receiving the wrong data, re-initialise, or change positions unexpectedly. If your data model uses the `key` property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction: + +```javascript +// Data model +var users = [ + {id: 1, name: "John", key: 'a'}, + {id: 2, name: "Mary", key: 'b'}, +] + +// Later on... +users[0].key = 'c' + +// AVOID +users.map(function(user){ + // The component for John will be destroyed and recreated + return m(UserComponent, user) +}) + +// PREFER +users.map(function(user){ + // Key is specifically extracted: data model is given its own property + return m(UserComponent, {key: user.id, model: user}) +}) From 97c60f9046cc46cc81bd4d5b4f991a7b2ca6faf1 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 24 Feb 2017 13:35:42 +0000 Subject: [PATCH 08/12] Close code blocks! --- docs/keys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/keys.md b/docs/keys.md index cfca7cdb..d6b16f8c 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -203,3 +203,4 @@ users.map(function(user){ // Key is specifically extracted: data model is given its own property return m(UserComponent, {key: user.id, model: user}) }) +``` From 60e8f307f19a0106722b3d7c05e14ae7b3fab456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Sat, 25 Feb 2017 21:53:51 +0100 Subject: [PATCH 09/12] Fix state initialization (#1652) * Add test-utils/components.js and its tests * Tests: group together tests with/without components * Tests: factory => closure components * Tests: add dummy forEach blocks around sections with components * Tests: render/test-components tweaks * Tests: Wrap some component definitions in `createComponent()` calls These are the ones that would have been tedious to automate. jscodeshift will handle the remaining ones * Tests: wrap the rest of the components definitions * Tests: enable the three kind of components in all related files but api/tests/test-route.js * Add test-utils/components.js to index.html where needed * Tests: Some more render/tests/test-component.js refactoring * Tests: enable tests for #1638 * fix #1638 * Test: ensure that recycled components get a fresh state * Tests: add a test for onbeforeupdate and recycled nodes * Fix recycled components initialization fix #1641 --- api/tests/index.html | 1 + api/tests/test-mount.js | 443 ++++--- api/tests/test-router.js | 2 +- render/render.js | 34 +- render/tests/index.html | 1 + render/tests/test-component.js | 1693 ++++++++++++++------------- render/tests/test-onbeforeremove.js | 68 +- render/tests/test-onbeforeupdate.js | 352 +++--- render/tests/test-onremove.js | 75 +- render/tests/test-render.js | 7 +- render/tests/test-updateNodes.js | 75 +- test-utils/components.js | 27 + test-utils/tests/index.html | 2 + test-utils/tests/test-components.js | 54 + tests/test-api.js | 187 +-- 15 files changed, 1615 insertions(+), 1406 deletions(-) create mode 100644 test-utils/components.js create mode 100644 test-utils/tests/test-components.js diff --git a/api/tests/index.html b/api/tests/index.html index fd2557d5..37d313f4 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -13,6 +13,7 @@ + diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index 038f470a..4ba466c7 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var m = require("../../render/hyperscript") @@ -22,16 +23,6 @@ o.spec("mount", function() { render = coreRenderer($window).render }) - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - mount(null, {view: function() {}}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - o("throws on invalid component", function() { var threw = false try { @@ -42,227 +33,223 @@ o.spec("mount", function() { o(threw).equals(true) }) - o("renders into `root` (POJO component)", function() { - mount(root, { - view : function() { - return m("div") - } - }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - o(root.firstChild.nodeName).equals("DIV") - }) - - o("renders into `root` (class component)", function() { - function Cmp(){} - Cmp.prototype.view = function(){return m("div")} - mount(root, Cmp) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("renders into `root` factory (factory component)", function() { - mount(root, function(){ - return { - view : function() { - return m("div") + o("throws on invalid `root` DOM node", function() { + var threw = false + try { + mount(null, createComponent({view: function() {}})) + } catch (e) { + threw = true } - } - }) + o(threw).equals(true) + }) - o(root.firstChild.nodeName).equals("DIV") - }) - - o("mounting null unmounts", function() { - mount(root, { - view : function() { - return m("div") - } - }) - - mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("redraws on events", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, { - view : function() { - return m("div", { - oninit : oninit, - onupdate : onupdate, - onclick : onclick, - }) - } - }) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - done() - }, FRAME_BUDGET) - }) - - o("redraws several mount points on events", function(done, timeout) { - timeout(60) - - var onupdate0 = o.spy() - var oninit0 = o.spy() - var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() - var onclick1 = o.spy() - - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], { - view : function() { - return m("div", { - oninit : oninit0, - onupdate : onupdate0, - onclick : onclick0, - }) - } - }) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - mount(root.childNodes[1], { - view : function() { - return m("div", { - oninit : oninit1, - onupdate : onupdate1, - onclick : onclick1, - }) - } - }) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root.childNodes[0].firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root.childNodes[0].firstChild) - - setTimeout(function() { - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root.childNodes[1].firstChild.dispatchEvent(e) - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].firstChild) - - setTimeout(function() { - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - - }) - - o("event handlers can skip redraw", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, { - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false + o("renders into `root`", function() { + mount(root, createComponent({ + view : function() { + return m("div") } - }) - } + })) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("mounting null unmounts", function() { + mount(root, createComponent({ + view : function() { + return m("div") + } + })) + + mount(root, null) + + o(root.childNodes.length).equals(0) + }) + + o("redraws on events", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + mount(root, createComponent({ + view : function() { + return m("div", { + oninit : oninit, + onupdate : onupdate, + onclick : onclick, + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + + o("redraws several mount points on events", function(done, timeout) { + timeout(60) + + var onupdate0 = o.spy() + var oninit0 = o.spy() + var onclick0 = o.spy() + var onupdate1 = o.spy() + var oninit1 = o.spy() + var onclick1 = o.spy() + + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + render(root, [ + m("#child0"), + m("#child1") + ]) + + mount(root.childNodes[0], createComponent({ + view : function() { + return m("div", { + oninit : oninit0, + onupdate : onupdate0, + onclick : onclick0, + }) + } + })) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + mount(root.childNodes[1], createComponent({ + view : function() { + return m("div", { + oninit : oninit1, + onupdate : onupdate1, + onclick : onclick1, + }) + } + })) + + o(oninit1.callCount).equals(1) + o(onupdate1.callCount).equals(0) + + root.childNodes[0].firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root.childNodes[0].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) + + root.childNodes[1].firstChild.dispatchEvent(e) + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root.childNodes[1].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + + }) + + o("event handlers can skip redraw", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + mount(root, createComponent({ + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate, + onclick: function(e) { + e.redraw = false + } + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + // Wrapped to ensure no redraw fired + setTimeout(function() { + o(onupdate.callCount).equals(0) + + done() + }, FRAME_BUDGET) + }) + + o("redraws when the render function is run", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + + mount(root, createComponent({ + view : function() { + return m("div", { + oninit: oninit, + onupdate: onupdate + }) + } + })) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + redrawService.redraw() + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + + o("throttles", function(done, timeout) { + timeout(200) + + var i = 0 + mount(root, createComponent({view: function() {i++}})) + var before = i + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + + var after = i + + setTimeout(function(){ + o(before).equals(1) // mounts synchronously + o(after).equals(1) // throttles rest + o(i).equals(2) + done() + },40) + }) }) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - // Wrapped to ensure no redraw fired - setTimeout(function() { - o(onupdate.callCount).equals(0) - - done() - }, FRAME_BUDGET) }) - - o("redraws when the render function is run", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - - mount(root, { - view : function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - }) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - redrawService.redraw() - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - done() - }, FRAME_BUDGET) - }) - - o("throttles", function(done, timeout) { - timeout(200) - - var i = 0 - mount(root, {view: function() {i++}}) - var before = i - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - var after = i - - setTimeout(function(){ - o(before).equals(1) // mounts synchronously - o(after).equals(1) // throttles rest - o(i).equals(2) - done() - },40) - }) -}) +}) \ No newline at end of file diff --git a/api/tests/test-router.js b/api/tests/test-router.js index c624789b..f43605e1 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -82,7 +82,7 @@ o.spec("route", function() { }) - o("routed mount points can redraw synchronously (factory component)", function() { + o("routed mount points can redraw synchronously (closure component)", function() { var view = o.spy() function Cmp() {return {view: view}} diff --git a/render/render.js b/render/render.js index 483c2637..f5d35085 100644 --- a/render/render.js +++ b/render/render.js @@ -20,8 +20,8 @@ module.exports = function($window) { } function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": return createText(parent, vnode, nextSibling) case "<": return createHTML(parent, vnode, nextSibling) @@ -100,7 +100,7 @@ module.exports = function($window) { } return element } - function createComponent(parent, vnode, hooks, ns, nextSibling) { + function initComponent(vnode, hooks) { var sentinel if (typeof vnode.tag === "function") { vnode.state = null @@ -116,10 +116,13 @@ module.exports = function($window) { sentinel.$$reentrantLock$$ = true } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null - + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -232,11 +235,12 @@ module.exports = function($window) { if (oldTag === tag) { vnode.state = old.state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) initLifecycle(vnode.attrs, vnode, hooks) + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break @@ -306,8 +310,13 @@ module.exports = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) - updateLifecycle(vnode.state, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) @@ -566,11 +575,10 @@ module.exports = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old) diff --git a/render/tests/index.html b/render/tests/index.html index 480b8b7c..b978ae6f 100644 --- a/render/tests/index.html +++ b/render/tests/index.html @@ -8,6 +8,7 @@ + diff --git a/render/tests/test-component.js b/render/tests/test-component.js index ed70fe7d..0ff4e07e 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -13,823 +14,901 @@ o.spec("component", function() { render = vdom($window).render }) - o.spec("basics", function() { - o("works", function() { - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} - - render(root, [node]) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("receives arguments", function() { - var component = { - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs, text: vnode.text} - } - } - var node = {tag: component, attrs: {id: "a"}, text: "b"} - - render(root, [node]) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("updates", function() { - var component = { - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs, text: vnode.text} - } - } - render(root, [{tag: component, attrs: {id: "a"}, text: "b"}]) - render(root, [{tag: component, attrs: {id: "c"}, text: "d"}]) - - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("c") - o(root.firstChild.firstChild.nodeValue).equals("d") - }) - o("updates root from null", function() { - var visible = false - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : null - } - } - render(root, [{tag: component}]) - visible = true - render(root, [{tag: component}]) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root from primitive", function() { - var visible = false - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : false - } - } - render(root, [{tag: component}]) - visible = true - render(root, [{tag: component}]) - - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root to null", function() { - var visible = true - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : null - } - } - render(root, [{tag: component}]) - visible = false - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("updates root to primitive", function() { - var visible = true - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : false - } - } - render(root, [{tag: component}]) - visible = false - render(root, [{tag: component}]) - - o(root.firstChild.nodeValue).equals("") - }) - o("updates root from null to null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("removes", function() { - var component = { - view: function(vnode) { - return {tag: "div"} - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("svg works when creating across component boundary", function() { - var component = { - view: function(vnode) { - return {tag: "g"} - } - } - render(root, [{tag: "svg", children: [{tag: component}]}]) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("svg works when updating across component boundary", function() { - var component = { - view: function(vnode) { - return {tag: "g"} - } - } - render(root, [{tag: "svg", children: [{tag: component}]}]) - render(root, [{tag: "svg", children: [{tag: component}]}]) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - }) - o.spec("return value", function() { - o("can return fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can return string", function() { - var component = { - view: function(vnode) { - return "a" - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can return falsy string", function() { - var component = { - view: function(vnode) { - return "" - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return number", function() { - var component = { - view: function(vnode) { - return 1 - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("1") - }) - o("can return falsy number", function() { - var component = { - view: function(vnode) { - return 0 - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("0") - }) - o("can return boolean", function() { - var component = { - view: function(vnode) { - return true - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("true") - }) - o("can return falsy boolean", function() { - var component = { - view: function(vnode) { - return false - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("can return undefined", function() { - var component = { - view: function(vnode) { - return undefined - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("throws a custom error if it returns itself", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var component = { - view: function(vnode) { - return vnode - } - } - try { - render(root, [{tag: component}]) - } - catch (e) { - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - }) - o("can update when returning fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can update when returning primitive", function() { - var component = { - view: function(vnode) { - return "a" - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can update when returning null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("can remove when returning fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("can remove when returning primitive", function() { - var component = { - view: function(vnode) { - return "a" - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - }) - o.spec("lifecycle", function() { - o("calls oninit", function() { - var called = 0 - var component = { - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit when returning fragment", function() { - var called = 0 - var component = { - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit before view", function() { - var viewCalled = false - - render(root, { - tag: { - view: function() { - viewCalled = true - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - }, - oninit: function(vnode) { - o(viewCalled).equals(false) - }, - } - }) - }) - o("does not calls oninit on redraw", function() { - var init = o.spy() - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - }, - oninit: init, - } - - function view() { - return {tag: component} - } - - render(root, view()) - render(root, view()) - - o(init.callCount).equals(1) - }) - o("calls oncreate", function() { - var called = 0 - var component = { - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("does not calls oncreate on redraw", function() { - var create = o.spy() - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - }, - oncreate: create, - } - - function view() { - return {tag: component} - } - - render(root, view()) - render(root, view()) - - o(create.callCount).equals(1) - }) - o("calls oncreate when returning fragment", function() { - var called = 0 - var component = { - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate", function() { - var called = 0 - var component = { - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, [{tag: component}]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate when returning fragment", function() { - var called = 0 - var component = { - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, [{tag: component}]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onremove", function() { - var called = 0 - var component = { - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onremove when returning fragment", function() { - var called = 0 - var component = { - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove", function() { - var called = 0 - var component = { - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove when returning fragment", function() { - var called = 0 - var component = { - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("does not recycle when there's an onupdate", function() { - var component = { - onupdate: function() {}, - view: function() { - return {tag: "div"} - } - } - var update = o.spy() - var vnode = {tag: component, key: 1} - var updated = {tag: component, key: 1} - - render(root, [vnode]) - render(root, []) - render(root, [updated]) - - o(vnode.dom).notEquals(updated.dom) - }) - }) - o.spec("state", function() { - o("copies state", function() { - var called = 0 - var data = {a: 1} - var component = { - data: data, - oninit: init, - view: function() { - return "" - } - } - - render(root, [{tag: component}]) - - 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() { - var called = 0 - var body = {a: 1} - var data = [body] - var component = { - data: data, - oninit: init, - view: function() { - return "" - } - } - - render(root, [{tag: component}]) - - function init(vnode) { - o(vnode.state.data).equals(data) - o(vnode.state.data[0]).equals(body) - } - }) - }) - o.spec("Alternative ways to specify componenents", function() { - o("Classes can be used as components", function() { - function MyComponent(vnode){ - o(vnode.state).equals(null) - } - var proto = MyComponent.prototype - - var context - - proto.oninit = o.spy(function(vnode) { - o(this).equals(vnode.state) - context = this - }) - proto.oncreate = o.spy() - proto.onbeforeupdate = o.spy() - proto.onupdate = o.spy() - proto.onbeforeremove = o.spy() - proto.onremove = o.spy() - proto.view = o.spy(function() { - return "" - }) - - render(root, [{tag: MyComponent}]) - - o(context instanceof MyComponent).equals(true) - - o(proto.view.callCount).equals(1) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(0) - o(proto.onupdate.callCount).equals(0) - o(proto.onbeforeremove.callCount).equals(0) - o(proto.onremove.callCount).equals(0) - - render(root, [{tag: MyComponent}]) - - o(proto.view.callCount).equals(2) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(1) - o(proto.onupdate.callCount).equals(1) - o(proto.onbeforeremove.callCount).equals(0) - o(proto.onremove.callCount).equals(0) - - render(root, []) - - o(proto.view.callCount).equals(2) - o(proto.oncreate.callCount).equals(1) - o(proto.onbeforeupdate.callCount).equals(1) - o(proto.onupdate.callCount).equals(1) - o(proto.onbeforeremove.callCount).equals(1) - o(proto.onremove.callCount).equals(1) - - o(proto.oninit.this).equals(context) - o(proto.view.this).equals(context) - o(proto.oncreate.this).equals(context) - o(proto.onbeforeupdate.this).equals(context) - o(proto.onupdate.this).equals(context) - o(proto.onbeforeremove.this).equals(context) - o(proto.onremove.this).equals(context) - - o(proto.oninit.args.length).equals(1) - o(proto.view.args.length).equals(1) - o(proto.oncreate.args.length).equals(1) - o(proto.onbeforeupdate.args.length).equals(2) - o(proto.onupdate.args.length).equals(1) - o(proto.onbeforeremove.args.length).equals(1) - o(proto.onremove.args.length).equals(1) - }) - o("Factory functions can be used as components", function() { - var state, context - function component(vnode) { - o(vnode.state).equals(null) - - return state = { - oninit: o.spy(function(vnode) { - o(this).equals(vnode.state) - context = this - }), - oncreate: o.spy(), - onbeforeupdate: o.spy(), - onupdate: o.spy(), - onbeforeremove: o.spy(), - onremove: o.spy(), - view: o.spy(function() { - return "" + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o.spec("basics", function() { + o("works", function() { + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } }) + var node = {tag: component} + + render(root, [node]) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("receives arguments", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs, text: vnode.text} + } + }) + var node = {tag: component, attrs: {id: "a"}, text: "b"} + + render(root, [node]) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("updates", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs, text: vnode.text} + } + }) + render(root, [{tag: component, attrs: {id: "a"}, text: "b"}]) + render(root, [{tag: component, attrs: {id: "c"}, text: "d"}]) + + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("c") + o(root.firstChild.firstChild.nodeValue).equals("d") + }) + o("updates root from null", function() { + var visible = false + var component = createComponent({ + view: function(vnode) { + return visible ? {tag: "div"} : null + } + }) + render(root, [{tag: component}]) + visible = true + render(root, [{tag: component}]) + + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root from primitive", function() { + var visible = false + var component = createComponent({ + view: function(vnode) { + return visible ? {tag: "div"} : false + } + }) + render(root, [{tag: component}]) + visible = true + render(root, [{tag: component}]) + + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root to null", function() { + var visible = true + var component = createComponent({ + view: function(vnode) { + return visible ? {tag: "div"} : null + } + }) + render(root, [{tag: component}]) + visible = false + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("updates root to primitive", function() { + var visible = true + var component = createComponent({ + view: function(vnode) { + return visible ? {tag: "div"} : false + } + }) + render(root, [{tag: component}]) + visible = false + render(root, [{tag: component}]) + + o(root.firstChild.nodeValue).equals("") + }) + o("updates root from null to null", function() { + var component = createComponent({ + view: function(vnode) { + return null + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("removes", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "div"} + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + render(root, [{tag: "div", key: 2}]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("svg works when creating across component boundary", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "g"} + } + }) + render(root, [{tag: "svg", children: [{tag: component}]}]) + + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("svg works when updating across component boundary", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "g"} + } + }) + render(root, [{tag: "svg", children: [{tag: component}]}]) + render(root, [{tag: "svg", children: [{tag: component}]}]) + + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + }) + o.spec("return value", function() { + o("can return fragments", function() { + var component = createComponent({ + view: function(vnode) { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can return string", function() { + var component = createComponent({ + view: function(vnode) { + return "a" + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can return falsy string", function() { + var component = createComponent({ + view: function(vnode) { + return "" + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + }) + o("can return number", function() { + var component = createComponent({ + view: function(vnode) { + return 1 + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("1") + }) + o("can return falsy number", function() { + var component = createComponent({ + view: function(vnode) { + return 0 + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("0") + }) + o("can return boolean", function() { + var component = createComponent({ + view: function(vnode) { + return true + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("true") + }) + o("can return falsy boolean", function() { + var component = createComponent({ + view: function(vnode) { + return false + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + }) + o("can return null", function() { + var component = createComponent({ + view: function(vnode) { + return null + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("can return undefined", function() { + var component = createComponent({ + view: function(vnode) { + return undefined + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("throws a custom error if it returns itself", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var component = createComponent({ + view: function(vnode) { + return vnode + } + }) + try { + render(root, [{tag: component}]) + } + catch (e) { + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + }) + o("can update when returning fragments", function() { + var component = createComponent({ + view: function(vnode) { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can update when returning primitive", function() { + var component = createComponent({ + view: function(vnode) { + return "a" + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can update when returning null", function() { + var component = createComponent({ + view: function(vnode) { + return null + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("can remove when returning fragments", function() { + var component = createComponent({ + view: function(vnode) { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + + render(root, [{tag: "div", key: 2}]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("can remove when returning primitive", function() { + var component = createComponent({ + view: function(vnode) { + return "a" + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + + render(root, [{tag: "div", key: 2}]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + }) + o.spec("lifecycle", function() { + o("calls oninit", function() { + var called = 0 + var component = createComponent({ + oninit: function(vnode) { + called++ + + o(vnode.tag).equals(component) + o(vnode.dom).equals(undefined) + o(root.childNodes.length).equals(0) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls oninit when returning fragment", function() { + var called = 0 + var component = createComponent({ + oninit: function(vnode) { + called++ + + o(vnode.tag).equals(component) + o(vnode.dom).equals(undefined) + o(root.childNodes.length).equals(0) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls oninit before view", function() { + var viewCalled = false + + render(root, createComponent({ + tag: { + view: function() { + viewCalled = true + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + }, + oninit: function(vnode) { + o(viewCalled).equals(false) + }, + } + })) + }) + o("does not calls oninit on redraw", function() { + var init = o.spy() + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + }, + oninit: init, + }) + + function view() { + return {tag: component} + } + + render(root, view()) + render(root, view()) + + o(init.callCount).equals(1) + }) + o("calls oncreate", function() { + var called = 0 + var component = createComponent({ + oncreate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("does not calls oncreate on redraw", function() { + var create = o.spy() + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + }, + oncreate: create, + }) + + function view() { + return {tag: component} + } + + render(root, view()) + render(root, view()) + + o(create.callCount).equals(1) + }) + o("calls oncreate when returning fragment", function() { + var called = 0 + var component = createComponent({ + oncreate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onupdate", function() { + var called = 0 + var component = createComponent({ + onupdate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, [{tag: component}]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onupdate when returning fragment", function() { + var called = 0 + var component = createComponent({ + onupdate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, [{tag: component}]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onremove", function() { + var called = 0 + var component = createComponent({ + onremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onremove when returning fragment", function() { + var called = 0 + var component = createComponent({ + onremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onbeforeremove", function() { + var called = 0 + var component = createComponent({ + onbeforeremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onbeforeremove when returning fragment", function() { + var called = 0 + var component = createComponent({ + onbeforeremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("does not recycle when there's an onupdate", function() { + var component = createComponent({ + onupdate: function() {}, + view: function() { + return {tag: "div"} + } + }) + var update = o.spy() + var vnode = {tag: component, key: 1} + var updated = {tag: component, key: 1} + + render(root, [vnode]) + render(root, []) + render(root, [updated]) + + o(vnode.dom).notEquals(updated.dom) + }) + o("lifecycle timing megatest (for a single component)", function() { + var methods = { + view: o.spy(function() { + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + // the `attrs` hooks are called before the component ones + attrs[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount + 1) + }) + methods[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount) + }) + }) + + var component = createComponent(methods) + + o(methods.view.callCount).equals(0) + o(methods.oninit.callCount).equals(0) + o(methods.oncreate.callCount).equals(0) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(1) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, []) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(1) + o(methods.onremove.callCount).equals(1) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + }) + o("hook state and arguments validation", function(){ + var methods = { + view: o.spy(function(vnode) { + o(this).equals(vnode.state) + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + attrs[hook] = o.spy(function(vnode){ + o(this).equals(vnode.state)(hook) + }) + methods[hook] = o.spy(function(vnode){ + o(this).equals(vnode.state) + }) + }) + + var component = createComponent(methods) + + render(root, [{tag: component, attrs: attrs}]) + render(root, [{tag: component, attrs: attrs}]) + render(root, []) + + hooks.forEach(function(hook) { + o(attrs[hook].this).equals(methods.view.this)(hook) + o(methods[hook].this).equals(methods.view.this)(hook) + }) + + o(methods.view.args.length).equals(1) + o(methods.oninit.args.length).equals(1) + o(methods.oncreate.args.length).equals(1) + o(methods.onbeforeupdate.args.length).equals(2) + o(methods.onupdate.args.length).equals(1) + o(methods.onbeforeremove.args.length).equals(1) + o(methods.onremove.args.length).equals(1) + + hooks.forEach(function(hook) { + o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) + }) + }) + o("recycled components get a fresh state", function() { + var step = 0 + var firstState + var view = o.spy(function(vnode) { + if (step === 0) { + firstState = vnode.state + } else { + o(vnode.state).notEquals(firstState) + } + return {tag: 'div'} + }) + var component = createComponent({view: view}) + + render(root, [{tag: 'div', children: [{tag: component, key: 1}]}]) + var child = root.firstChild.firstChild + render(root, []) + step = 1 + render(root, [{tag: 'div', children: [{tag: component, key: 1}]}]) + + o(child).equals(root.firstChild.firstChild) + o(view.callCount).equals(2) + }) + }) + o.spec("state", function() { + o("initializes state", function() { + var called = 0 + var data = {a: 1} + var component = createComponent(createComponent({ + data: data, + oninit: init, + view: function() { + return "" + } + })) + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + } + }) + o('state proxies to the component object/prototype', function() { + var called = 0 + var body = {a: 1} + var data = [body] + var component = createComponent(createComponent({ + data: data, + oninit: init, + view: function() { + return "" + } + })) + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + o(vnode.state.data[0]).equals(body) + } + }) + }) + }) + }) + o.spec("Tests specific to certain component kinds", function() { + o.spec("state", function() { + o("POJO", function() { + var called = 0 + var data = {} + var component = { + data: data, + oninit: init, + view: function() { + return "" + } } - } - render(root, [{tag: component}]) + render(root, [{tag: component}]) - o(state).equals(context) + function init(vnode) { + o(vnode.state.data).equals(data) - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(1) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(0) - o(state.onupdate.callCount).equals(0) - o(state.onbeforeremove.callCount).equals(0) - o(state.onremove.callCount).equals(0) + //inherits state via prototype + component.x = 1 + o(vnode.state.x).equals(1) + } + }) + o("Constructible", function() { + var oninit = o.spy() + var component = o.spy(function(vnode){ + o(vnode.state).equals(null) + o(oninit.callCount).equals(0) + }) + var view = o.spy(function(){ + o(this instanceof component).equals(true) + return "" + }) + component.prototype.view = view + component.prototype.oninit = oninit - render(root, [{tag: component}]) + var context - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(2) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(1) - o(state.onupdate.callCount).equals(1) - o(state.onbeforeremove.callCount).equals(0) - o(state.onremove.callCount).equals(0) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) - render(root, []) + o(component.callCount).equals(1) + o(oninit.callCount).equals(2) + o(view.callCount).equals(2) + }) + o("Closure", function() { + var state + var oninit = o.spy() + var view = o.spy(function() { + o(this).equals(state) + return "" + }) + var component = o.spy(function(vnode) { + o(vnode.state).equals(null) + o(oninit.callCount).equals(0) + return state = { + view: view + } + }) - o(state.oninit.callCount).equals(1) - o(state.view.callCount).equals(2) - o(state.oncreate.callCount).equals(1) - o(state.onbeforeupdate.callCount).equals(1) - o(state.onupdate.callCount).equals(1) - o(state.onbeforeremove.callCount).equals(1) - o(state.onremove.callCount).equals(1) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) - o(state.oninit.this).equals(state) - o(state.view.this).equals(state) - o(state.oncreate.this).equals(state) - o(state.onbeforeupdate.this).equals(state) - o(state.onupdate.this).equals(state) - o(state.onbeforeremove.this).equals(state) - o(state.onremove.this).equals(state) - - o(state.oninit.args.length).equals(1) - o(state.view.args.length).equals(1) - o(state.oncreate.args.length).equals(1) - o(state.onbeforeupdate.args.length).equals(2) - o(state.onupdate.args.length).equals(1) - o(state.onbeforeremove.args.length).equals(1) - o(state.onremove.args.length).equals(1) + o(component.callCount).equals(1) + o(oninit.callCount).equals(1) + o(view.callCount).equals(2) + }) }) }) }) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index c9af4894..4395f90a 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -2,6 +2,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var Promise = require("../../promise/promise") @@ -169,39 +170,44 @@ o.spec("onbeforeremove", function() { done() }) }) - 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(){return Promise.resolve()} - var component = { - onbeforeremove: onbeforeremove, - onremove: onremove, - view: function() {}, - } - render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}]) - render(root, []) - callAsync(function() { - o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - 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, []) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + 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(){return Promise.resolve()} + var component = createComponent({ + onbeforeremove: onbeforeremove, + onremove: onremove, + view: function() {}, + }) + render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}]) + render(root, []) + callAsync(function() { + o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` + 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 = createComponent({ + 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(0) - callAsync(function() { - o(onremove.callCount).equals(1) - done() + callAsync(function() { + o(onremove.callCount).equals(1) + done() + }) + }) }) }) }) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 8b326b8c..93643409 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -56,86 +57,6 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.nodeValue).equals("a") }) - o("prevents update in component", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", children: vnode.children} - }, - } - var vnode = {tag: component, children: [{tag: "#", children: "a"}]} - var updated = {tag: component, children: [{tag: "#", children: "b"}]} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.firstChild.nodeValue).equals("a") - }) - - o("prevents update if returning false in component and false in vnode", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("a") - }) - - o("does not prevent update if returning true in component and true in vnode", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - - o("does not prevent update if returning false in component but true in vnode", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - - o("does not prevent update if returning true in component but false in vnode", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("does not prevent update if returning true", function() { var onbeforeupdate = function() {return true} var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -147,22 +68,6 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.attributes["id"].nodeValue).equals("b") }) - o("does not prevent update if returning true from component", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("accepts arguments for comparison", function() { var count = 0 var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -184,33 +89,6 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.attributes["id"].nodeValue).equals("b") }) - o("accepts arguments for comparison in component", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - var count = 0 - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} - - render(root, [vnode]) - render(root, [updated]) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("is not called on creation", function() { var count = 0 var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -226,28 +104,6 @@ o.spec("onbeforeupdate", function() { o(count).equals(0) }) - o("is not called on component creation", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - - var count = 0 - var vnode = {tag: "div", attrs: {id: "a"}} - var updated = {tag: "div", attrs: {id: "b"}} - - render(root, [vnode]) - - function onbeforeupdate(vnode, old) { - count++ - return true - } - - o(count).equals(0) - }) - o("is called only once on update", function() { var count = 0 var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -264,26 +120,192 @@ o.spec("onbeforeupdate", function() { o(count).equals(1) }) - o("is called only once on component update", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } + o("doesn't fire on recycled nodes", function() { + var onbeforeupdate = o.spy() + var vnodes = [{tag: "div", key: 1}] + var temp = [] + var updated = [{tag: "div", key: 1, attrs: {onbeforeupdate: onbeforeupdate}}] - var count = 0 - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} + render(root, vnodes) + render(root, temp) + render(root, updated) - render(root, [vnode]) - render(root, [updated]) - - function onbeforeupdate(vnode, old) { - count++ - return true - } - - o(count).equals(1) + o(vnodes[0].dom).equals(updated[0].dom) + o(updated[0].dom.nodeName).equals("DIV") + o(onbeforeupdate.callCount).equals(0) }) -}) + + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("prevents update in component", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", children: vnode.children} + }, + }) + var vnode = {tag: component, children: [{tag: "#", children: "a"}]} + var updated = {tag: component, children: [{tag: "#", children: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.firstChild.nodeValue).equals("a") + }) + + o("prevents update if returning false in component and false in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("a") + }) + + o("does not prevent update if returning true in component and true in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning false in component but true in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning true in component but false in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning true from component", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("accepts arguments for comparison in component", function() { + var component = createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + var count = 0 + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + function onbeforeupdate(vnode, old) { + count++ + + o(old.attrs.id).equals("a") + o(vnode.attrs.id).equals("b") + + return old.attrs.id !== vnode.attrs.id + } + + o(count).equals(1) + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("is not called on component creation", function() { + var component = createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + + var count = 0 + var vnode = {tag: "div", attrs: {id: "a"}} + var updated = {tag: "div", attrs: {id: "b"}} + + render(root, [vnode]) + + function onbeforeupdate(vnode, old) { + count++ + return true + } + + o(count).equals(0) + }) + + o("is called only once on component update", function() { + var component = createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + + var count = 0 + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + function onbeforeupdate(vnode, old) { + count++ + return true + } + + o(count).equals(1) + }) + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index d3f423fc..a7f88a6b 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") @@ -80,39 +81,6 @@ o.spec("onremove", function() { o(remove.this).equals(vnode.state) o(remove.args[0]).equals(vnode) }) - o("calls onremove on nested component", function() { - var spy = o.spy() - var comp = { - view: function() {return m(outer)} - } - var outer = { - view: function() {return m(inner)} - } - var inner = { - onremove: spy, - view: function() {return m("div")} - } - render(root, {tag: comp}) - render(root, null) - - o(spy.callCount).equals(1) - }) - o("calls onremove on nested component child", function() { - var spy = o.spy() - var comp = { - view: function() {return m(outer)} - } - var outer = { - view: function() {return m(inner, m("a", {onremove: spy}))} - } - var inner = { - view: function(vnode) {return m("div", vnode.children)} - } - render(root, {tag: comp}) - render(root, null) - - o(spy.callCount).equals(1) - }) o("does not set onremove as an event handler", function() { var remove = o.spy() var vnode = {tag: "div", attrs: {onremove: remove}, children: []} @@ -145,4 +113,43 @@ o.spec("onremove", function() { o(vnode.dom).notEquals(updated.dom) }) -}) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("calls onremove on nested component", function() { + var spy = o.spy() + var comp = createComponent({ + view: function() {return m(outer)} + }) + var outer = createComponent({ + view: function() {return m(inner)} + }) + var inner = createComponent({ + onremove: spy, + view: function() {return m("div")} + }) + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) + o("calls onremove on nested component child", function() { + var spy = o.spy() + var comp = createComponent({ + view: function() {return m(outer)} + }) + var outer = createComponent({ + view: function() {return m(inner, m("a", {onremove: spy}))} + }) + var inner = createComponent({ + view: function(vnode) {return m("div", vnode.children)} + }) + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 2f3ebb9a..82e5ddba 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -118,7 +119,7 @@ o.spec("render", function() { o(oninit.callCount).equals(0) o(onbeforeupdate.callCount).equals(0) }) - o("does not try to re-initialize a factory component whose view has thrown", function() { + o("does not try to re-initialize a closure component whose view has thrown", function() { var oninit = o.spy() var onbeforeupdate = o.spy() function A() { @@ -141,7 +142,7 @@ o.spec("render", function() { o(oninit.callCount).equals(1) o(onbeforeupdate.callCount).equals(0) }) - o("does not try to re-initialize a factory component whose oninit has thrown", function() { + o("does not try to re-initialize a closure component whose oninit has thrown", function() { var oninit = o.spy(function(vnode) {throw new Error("error")}) var onbeforeupdate = o.spy() function A() { @@ -164,7 +165,7 @@ o.spec("render", function() { o(oninit.callCount).equals(1) o(onbeforeupdate.callCount).equals(0) }) - o("does not try to re-initialize a factory component whose factory has thrown", function() { + o("does not try to re-initialize a closure component whose closure has thrown", function() { function A() { throw new Error("error") } diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index b0e8c337..c58a29b4 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -838,38 +839,6 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) - o("fragment child toggles from null when followed by null component then tag", function() { - var component = {view: function() {return null}} - var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] - var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}] - var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("fragment child toggles from null in component when followed by null component then tag", function() { - var flag = true - var a = {view: function() {return flag ? {tag: "a"} : null}} - var b = {view: function() {return null}} - var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - - render(root, vnodes) - flag = false - render(root, temp) - flag = true - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("S") - }) o("cached, non-keyed nodes skip diff", function () { var onupdate = o.spy(); var cached = {tag:"a", attrs:{onupdate: onupdate}} @@ -926,7 +895,7 @@ o.spec("updateNodes", function() { o(update.callCount).equals(2) o(remove.callCount).equals(0) }) - o("component is recreated if key changes to undefined", function () { + o("node is recreated if key changes to undefined", function () { var vnode = {tag: "b", key: 1} var updated = {tag: "b"} @@ -936,4 +905,42 @@ o.spec("updateNodes", function() { o(vnode.dom).notEquals(updated.dom) }) -}) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("fragment child toggles from null when followed by null component then tag", function() { + var component = createComponent({view: function() {return null}}) + var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}] + var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("fragment child toggles from null in component when followed by null component then tag", function() { + var flag = true + var a = createComponent({view: function() {return flag ? {tag: "a"} : null}}) + var b = createComponent({view: function() {return null}}) + var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + + render(root, vnodes) + flag = false + render(root, temp) + flag = true + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("S") + }) + }) + }) +}) \ No newline at end of file diff --git a/test-utils/components.js b/test-utils/components.js new file mode 100644 index 00000000..c25ad5da --- /dev/null +++ b/test-utils/components.js @@ -0,0 +1,27 @@ +module.exports = [ + { + kind: 'POJO', + create: function(methods) { + var res = {view: function() {return {tag:'div'}}} + Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) + return res + } + }, { + kind: 'constructible', + create: function(methods) { + function res(){} + res.prototype.view = function() {return {tag:'div'}} + Object.keys(methods || {}).forEach(function(m){res.prototype[m] = methods[m]}) + return res + } + }, { + kind: 'closure', + create: function(methods) { + return function() { + var res = {view: function() {return {tag:'div'}}} + Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) + return res + } + } + } +] diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html index e24fa2f8..51b04d73 100644 --- a/test-utils/tests/index.html +++ b/test-utils/tests/index.html @@ -14,12 +14,14 @@ + + diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js new file mode 100644 index 00000000..cc9a091b --- /dev/null +++ b/test-utils/tests/test-components.js @@ -0,0 +1,54 @@ +"use strict" + +var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") + +o.spec("test-utils/components", function() { + var test = o.spy(function(component) { + return function() { + o('works', function() { + o(typeof component.kind).equals('string') + + var methods = {oninit: function(){}, view: function(){}} + + var cmp1, cmp2 + + if (component.kind === "POJO") { + cmp1 = component.create() + cmp2 = component.create(methods) + } else if (component.kind === "constructible") { + cmp1 = new (component.create()) + cmp2 = new (component.create(methods)) + } else if (component.kind === "closure") { + cmp1 = component.create()() + cmp2 = component.create(methods)() + } else { + throw new Error("unexpected component kind") + } + + o(cmp1 != null).equals(true) + o(typeof cmp1.view).equals("function") + + var vnode = cmp1.view() + + o(vnode != null).equals(true) + o(vnode).deepEquals({tag: "div"}) + + if (component.kind !== 'constructible') { + o(cmp2).deepEquals(methods) + } else { + // deepEquals doesn't search the prototype, do it manually + o(cmp2 != null).equals(true) + o(cmp2.view).equals(methods.view) + o(cmp2.oninit).equals(methods.oninit) + } + }) + } + }) + o.after(function(){ + o(test.callCount).equals(3) + }) + components.forEach(function(component) { + o.spec(component.kind, test(component)) + }) +}) diff --git a/tests/test-api.js b/tests/test-api.js index 989b00a4..ec57a0f3 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -2,6 +2,7 @@ var o = require("../ospec/ospec") var browserMock = require("../test-utils/browserMock") +var components = require("../test-utils/components") o.spec("api", function() { var m @@ -68,95 +69,6 @@ o.spec("api", function() { o(query).equals("a=1&b=2") }) }) - o.spec("m.render", function() { - o("works", function() { - var root = window.document.createElement("div") - m.render(root, m("div")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.mount", function() { - o("works", function() { - var root = window.document.createElement("div") - m.mount(root, {view: function() {return m("div")}}) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.route", function() { - o("works", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.prefix", function(done) { - var root = window.document.createElement("div") - m.route.prefix("#") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.get", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(m.route.get()).equals("/a") - - done() - }, FRAME_BUDGET) - }) - o("m.route.set", function(done, timeout) { - timeout(100) - var root = window.document.createElement("div") - m.route(root, "/a", { - "/:id": {view: function() {return m("div")}} - }) - - setTimeout(function() { - m.route.set("/b") - setTimeout(function() { - o(m.route.get()).equals("/b") - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - }) - }) - o.spec("m.redraw", function() { - o("works", function(done) { - var count = 0 - var root = window.document.createElement("div") - m.mount(root, {view: function() {count++}}) - setTimeout(function() { - m.redraw() - - o(count).equals(2) - - done() - }, FRAME_BUDGET) - }) - }) o.spec("m.request", function() { o("works", function() { o(typeof m.request).equals("function") // TODO improve @@ -167,4 +79,99 @@ o.spec("api", function() { o(typeof m.jsonp).equals("function") // TODO improve }) }) -}) + o.spec("m.render", function() { + o("works", function() { + var root = window.document.createElement("div") + m.render(root, m("div")) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o.spec("m.mount", function() { + o("works", function() { + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {return m("div")}})) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + o.spec("m.route", function() { + o("works", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.prefix", function(done) { + var root = window.document.createElement("div") + m.route.prefix("#") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.get", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(m.route.get()).equals("/a") + + done() + }, FRAME_BUDGET) + }) + o("m.route.set", function(done, timeout) { + timeout(100) + var root = window.document.createElement("div") + m.route(root, "/a", { + "/:id": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + m.route.set("/b") + setTimeout(function() { + o(m.route.get()).equals("/b") + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + }) + }) + o.spec("m.redraw", function() { + o("works", function(done) { + var count = 0 + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {count++}})) + setTimeout(function() { + m.redraw() + + o(count).equals(2) + + done() + }, FRAME_BUDGET) + }) + }) + }) + }) +}) \ No newline at end of file From 64792cbb11fb774a26ecf26c2dafdcfe708089a8 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sat, 25 Feb 2017 20:55:13 +0000 Subject: [PATCH 10/12] Bundled output for commit 60e8f307f19a0106722b3d7c05e14ae7b3fab456 [skip ci] --- mithril.js | 33 ++++++++++++------- mithril.min.js | 86 +++++++++++++++++++++++++------------------------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/mithril.js b/mithril.js index 5d3c46e5..b7f11d41 100644 --- a/mithril.js +++ b/mithril.js @@ -368,8 +368,8 @@ var coreRenderer = function($window) { } function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": return createText(parent, vnode, nextSibling) case "<": return createHTML(parent, vnode, nextSibling) @@ -442,7 +442,7 @@ var coreRenderer = function($window) { } return element } - function createComponent(parent, vnode, hooks, ns, nextSibling) { + function initComponent(vnode, hooks) { var sentinel if (typeof vnode.tag === "function") { vnode.state = null @@ -457,9 +457,13 @@ var coreRenderer = function($window) { if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -570,11 +574,12 @@ var coreRenderer = function($window) { if (oldTag === tag) { vnode.state = old.state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) initLifecycle(vnode.attrs, vnode, hooks) + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break @@ -644,8 +649,13 @@ var coreRenderer = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) - updateLifecycle(vnode.state, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) @@ -897,11 +907,10 @@ var coreRenderer = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old) diff --git a/mithril.min.js b/mithril.min.js index 732ad9c3..ec786ea9 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,43 +1,43 @@ -new function(){function w(a,d,h,f,g,l){return{tag:a,key:d,attrs:h,children:f,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===H[a]){for(var d,h,f=[],g={};d=O.exec(a);){var l=d[1],k=d[2];""===l&&""!==k?h=k:"#"===l?g.id=k:"."===l?f.push(k):"["===d[3][0]&&((l=d[6])&&(l=l.replace(/\\(["'])/g, -"$1").replace(/\\\\/g,"\\")),"class"===d[4]?f.push(l):g[d[4]]=l||!0)}0c.indexOf("?")?"?":"&";c+=f+d}return c}function k(c){try{return""!==c?JSON.parse(c):null}catch(t){throw Error(c); -}}function q(c){return c.responseText}function p(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;dn.status||304===n.status||R.test(c.url))d(p(c.type, -b));else{var m=Error(n.responseText),a;for(a in b)m[a]=b[a];f(m)}}catch(C){f(C)}};h&&null!=c.data?n.send(c.data):n.send()});return!0===c.background?t:r(t)},jsonp:function(c,k){var q=h();c=f(c,k);var u=new d(function(d,f){var h=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,k=a.document.createElement("script");a[h]=function(f){k.parentNode.removeChild(k);d(p(c.type,f));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete a[h]};null== -c.data&&(c.data={});c.url=g(c.url,c.data);c.data[c.callbackKey||"callback"]=h;k.src=l(c.url,c.data);a.document.documentElement.appendChild(k)});return!0===c.background?u:q(u)},setCompletionCallback:function(a){u=a}}}(window,v),N=function(a){function d(a,b,c,d,f,g,k){for(;c=m&&t>=e;){var y=b[m],z=c[e];if(y!==z||f)if(null==y)m++;else if(null==z)e++;else if(y.key===z.key){var C=null!=x&&m>=b.length-x.length||null==x&&f;m++;e++;l(a,y,z,g,q(b,m,B),C,n);f&&y.tag===z.tag&&p(a,k(y),B)}else if(y=b[r],y!==z||f)if(null==y)r--;else if(null==z)e++;else if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f, -l(a,y,z,g,q(b,r+1,B),C,n),(f||e=m&&t>=e;){y=b[r];z=c[t];if(y!==z||f)if(null==y)r--;else{if(null!=z)if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f,l(a,y,z,g,q(b,r+1,B),C,n),f&&y.tag===z.tag&&p(a,k(y),B),null!=y.dom&&(B=y.dom),r--;else{if(!E){E=b;var y=r,C={},v;for(v=0;va.indexOf("?")?"?":"&";a+=e+d}return a}function h(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a); +}}function r(a){return a.responseText}function n(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dm.status||304===m.status||R.test(a.url))d(n(a.type, +f));else{var c=Error(m.responseText),q;for(q in f)c[q]=f[q];e(c)}}catch(p){e(p)}};l&&null!=a.data?m.send(a.data):m.send()});return!0===a.background?y:v(y)},jsonp:function(a,h){var v=l();a=e(a,h);var r=new d(function(d,e){var l=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,h=b.document.createElement("script");b[l]=function(e){h.parentNode.removeChild(h);d(n(a.type,e));delete b[l]};h.onerror=function(){h.parentNode.removeChild(h);e(Error("JSONP request failed"));delete b[l]};null== +a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||"callback"]=l;h.src=k(a.url,a.data);b.document.documentElement.appendChild(h)});return!0===a.background?r:v(r)},setCompletionCallback:function(b){v=b}}}(window,A),N=function(b){function d(f,c,q,b,a,d,e){for(;q=q&&y>=p;){var z=c[q],t=b[p];if(z!==t||e)if(null==z)q++;else if(null==t)p++;else if(z.key===t.key){var u=null!=D&&q>=c.length-D.length||null==D&&e;q++;p++;h(f,z,t,g,n(c,q,k),u,v);e&&z.tag===t.tag&&m(f,r(z),k)}else if(z=c[w],z!==t||e)if(null==z)w--;else if(null==t)p++;else if(z.key===t.key)u= +null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),(e||p=q&&y>=p;){z=c[w];t=b[y];if(z!==t||e)if(null==z)w--;else{if(null!=t)if(z.key===t.key)u=null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),e&&z.tag===t.tag&&m(f,r(z),k),null!=z.dom&&(k=z.dom),w--;else{if(!G){G=c;var z=w,u={},x;for(x=0;x Date: Sat, 25 Feb 2017 18:34:32 -0500 Subject: [PATCH 11/12] Add stream/scan, scanMerge types --- mithril.d.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mithril.d.ts b/mithril.d.ts index 37179e16..8864d60a 100644 --- a/mithril.d.ts +++ b/mithril.d.ts @@ -238,6 +238,18 @@ declare namespace Mithril { /** A special value that can be returned to stream callbacks to halt execution of downstreams. */ HALT: any; } + + interface StreamScan { + /** Creates a new stream with the results of calling the function on every incoming stream with and accumulator and the incoming value. */ + (fn: (acc: U, value: T) => U, acc: U, stream: Stream): Stream; + } + + interface StreamScanMerge { + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + (pairs: [Stream, (acc: U, value: T) => U][], acc: U): Stream; + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + (pairs: [Stream, (acc: U, value: any) => U][], acc: U): Stream; + } } declare module 'mithril' { @@ -284,3 +296,13 @@ declare module 'mithril/stream' { const s: Mithril.StreamFactory; export = s; } + +declare module 'mithril/stream/scan' { + const s: Mithril.StreamScan; + export = s; +} + +declare module 'mithril/stream/scanMerge' { + const sm: Mithril.StreamScanMerge; + export = sm; +} From 38db32e8b7c4f1837d4f696620651fce93d76654 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Thu, 2 Mar 2017 19:29:12 +0000 Subject: [PATCH 12/12] Bundled output for commit 9d6a5e51eb4cebb4de1a06a32f1ef3b52f265ebc [skip ci] --- mithril.js | 33 ++++++++++++------- mithril.min.js | 86 +++++++++++++++++++++++++------------------------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/mithril.js b/mithril.js index 5d3c46e5..b7f11d41 100644 --- a/mithril.js +++ b/mithril.js @@ -368,8 +368,8 @@ var coreRenderer = function($window) { } function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": return createText(parent, vnode, nextSibling) case "<": return createHTML(parent, vnode, nextSibling) @@ -442,7 +442,7 @@ var coreRenderer = function($window) { } return element } - function createComponent(parent, vnode, hooks, ns, nextSibling) { + function initComponent(vnode, hooks) { var sentinel if (typeof vnode.tag === "function") { vnode.state = null @@ -457,9 +457,13 @@ var coreRenderer = function($window) { if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -570,11 +574,12 @@ var coreRenderer = function($window) { if (oldTag === tag) { vnode.state = old.state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) initLifecycle(vnode.attrs, vnode, hooks) + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break @@ -644,8 +649,13 @@ var coreRenderer = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) - updateLifecycle(vnode.state, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) @@ -897,11 +907,10 @@ var coreRenderer = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old) diff --git a/mithril.min.js b/mithril.min.js index 732ad9c3..ec786ea9 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,43 +1,43 @@ -new function(){function w(a,d,h,f,g,l){return{tag:a,key:d,attrs:h,children:f,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===H[a]){for(var d,h,f=[],g={};d=O.exec(a);){var l=d[1],k=d[2];""===l&&""!==k?h=k:"#"===l?g.id=k:"."===l?f.push(k):"["===d[3][0]&&((l=d[6])&&(l=l.replace(/\\(["'])/g, -"$1").replace(/\\\\/g,"\\")),"class"===d[4]?f.push(l):g[d[4]]=l||!0)}0c.indexOf("?")?"?":"&";c+=f+d}return c}function k(c){try{return""!==c?JSON.parse(c):null}catch(t){throw Error(c); -}}function q(c){return c.responseText}function p(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;dn.status||304===n.status||R.test(c.url))d(p(c.type, -b));else{var m=Error(n.responseText),a;for(a in b)m[a]=b[a];f(m)}}catch(C){f(C)}};h&&null!=c.data?n.send(c.data):n.send()});return!0===c.background?t:r(t)},jsonp:function(c,k){var q=h();c=f(c,k);var u=new d(function(d,f){var h=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,k=a.document.createElement("script");a[h]=function(f){k.parentNode.removeChild(k);d(p(c.type,f));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete a[h]};null== -c.data&&(c.data={});c.url=g(c.url,c.data);c.data[c.callbackKey||"callback"]=h;k.src=l(c.url,c.data);a.document.documentElement.appendChild(k)});return!0===c.background?u:q(u)},setCompletionCallback:function(a){u=a}}}(window,v),N=function(a){function d(a,b,c,d,f,g,k){for(;c=m&&t>=e;){var y=b[m],z=c[e];if(y!==z||f)if(null==y)m++;else if(null==z)e++;else if(y.key===z.key){var C=null!=x&&m>=b.length-x.length||null==x&&f;m++;e++;l(a,y,z,g,q(b,m,B),C,n);f&&y.tag===z.tag&&p(a,k(y),B)}else if(y=b[r],y!==z||f)if(null==y)r--;else if(null==z)e++;else if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f, -l(a,y,z,g,q(b,r+1,B),C,n),(f||e=m&&t>=e;){y=b[r];z=c[t];if(y!==z||f)if(null==y)r--;else{if(null!=z)if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f,l(a,y,z,g,q(b,r+1,B),C,n),f&&y.tag===z.tag&&p(a,k(y),B),null!=y.dom&&(B=y.dom),r--;else{if(!E){E=b;var y=r,C={},v;for(v=0;va.indexOf("?")?"?":"&";a+=e+d}return a}function h(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a); +}}function r(a){return a.responseText}function n(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dm.status||304===m.status||R.test(a.url))d(n(a.type, +f));else{var c=Error(m.responseText),q;for(q in f)c[q]=f[q];e(c)}}catch(p){e(p)}};l&&null!=a.data?m.send(a.data):m.send()});return!0===a.background?y:v(y)},jsonp:function(a,h){var v=l();a=e(a,h);var r=new d(function(d,e){var l=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,h=b.document.createElement("script");b[l]=function(e){h.parentNode.removeChild(h);d(n(a.type,e));delete b[l]};h.onerror=function(){h.parentNode.removeChild(h);e(Error("JSONP request failed"));delete b[l]};null== +a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||"callback"]=l;h.src=k(a.url,a.data);b.document.documentElement.appendChild(h)});return!0===a.background?r:v(r)},setCompletionCallback:function(b){v=b}}}(window,A),N=function(b){function d(f,c,q,b,a,d,e){for(;q=q&&y>=p;){var z=c[q],t=b[p];if(z!==t||e)if(null==z)q++;else if(null==t)p++;else if(z.key===t.key){var u=null!=D&&q>=c.length-D.length||null==D&&e;q++;p++;h(f,z,t,g,n(c,q,k),u,v);e&&z.tag===t.tag&&m(f,r(z),k)}else if(z=c[w],z!==t||e)if(null==z)w--;else if(null==t)p++;else if(z.key===t.key)u= +null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),(e||p=q&&y>=p;){z=c[w];t=b[y];if(z!==t||e)if(null==z)w--;else{if(null!=t)if(z.key===t.key)u=null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),e&&z.tag===t.tag&&m(f,r(z),k),null!=z.dom&&(k=z.dom),w--;else{if(!G){G=c;var z=w,u={},x;for(x=0;x