From aeb1c41bdee58ef1809239da8900d9206108561c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 26 Mar 2017 21:33:30 +0200 Subject: [PATCH 1/9] treat functions with a view() method as POJO, not closure component --- render/render.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/render/render.js b/render/render.js index a048c460..835c2f5f 100644 --- a/render/render.js +++ b/render/render.js @@ -103,18 +103,17 @@ module.exports = function($window) { } function initComponent(vnode, hooks) { var sentinel - if (typeof vnode.tag === "function") { + if (typeof vnode.tag.view === "function") { + vnode.state = Object.create(vnode.tag) + sentinel = vnode.state.view + if (sentinel.$$reentrantLock$$ != null) return $emptyFragment + sentinel.$$reentrantLock$$ = true + } else { vnode.state = null sentinel = vnode.tag if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) - } else { - // For object literals since `Vnode()` always sets the `state` field. - vnode.state = Object.create(vnode.tag) - sentinel = vnode.state.view - if (sentinel.$$reentrantLock$$ != null) return $emptyFragment - sentinel.$$reentrantLock$$ = true } if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) From 7be2ff5feb2e4584be7d666a19e85a22e7ade956 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 26 Mar 2017 22:07:49 +0200 Subject: [PATCH 2/9] Strengthen the self-return prevention logic (for recycled nodes and updates) --- render/render.js | 3 ++- render/tests/test-component.js | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/render/render.js b/render/render.js index 835c2f5f..5e238d56 100644 --- a/render/render.js +++ b/render/render.js @@ -119,12 +119,12 @@ module.exports = function($window) { if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") 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) vnode.dom = vnode.instance.dom vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 @@ -317,6 +317,7 @@ module.exports = function($window) { initComponent(vnode, hooks) } else { vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) updateLifecycle(vnode.state, vnode, hooks) } diff --git a/render/tests/test-component.js b/render/tests/test-component.js index a3429cee..8fccb9c3 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -260,8 +260,9 @@ o.spec("component", function() { o(root.childNodes.length).equals(0) }) - o("throws a custom error if it returns itself", function() { + o("throws a custom error if it returns itself when created", function() { // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false var component = createComponent({ view: function(vnode) { return vnode @@ -271,10 +272,41 @@ o.spec("component", function() { render(root, [{tag: component}]) } catch (e) { + threw = true o(e instanceof Error).equals(true) // Call stack exception is a RangeError o(e instanceof RangeError).equals(false) } + o(threw).equals(true) + }) + o("throws a custom error if it returns itself when updated", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false + var init = true + var oninit = o.spy() + var component = createComponent({ + oninit: oninit, + view: function(vnode) { + if (init) return init = false + else return vnode + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + + try { + render(root, [{tag: component}]) + } + catch (e) { + threw = true + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + o(threw).equals(true) + o(oninit.callCount).equals(1) }) o("can update when returning fragments", function() { var component = createComponent({ From d69f6e37aa8fdf116b998487e4023ae19caaf07c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 26 Mar 2017 22:24:17 +0200 Subject: [PATCH 3/9] Add a shadow vnode._state field for view and hook lookups --- render/render.js | 19 ++++++++++--------- render/vnode.js | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/render/render.js b/render/render.js index 5e238d56..ce40b9d9 100644 --- a/render/render.js +++ b/render/render.js @@ -115,10 +115,10 @@ module.exports = function($window) { sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) } - + vnode._state = vnode.state if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - initLifecycle(vnode.state, vnode, hooks) - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + initLifecycle(vnode._state, vnode, hooks) + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") sentinel.$$reentrantLock$$ = null } @@ -234,6 +234,7 @@ module.exports = function($window) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state + vnode._state = old._state vnode.events = old.events if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { @@ -316,10 +317,10 @@ module.exports = function($window) { if (recycling) { initComponent(vnode, hooks) } else { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - updateLifecycle(vnode.state, vnode, hooks) + updateLifecycle(vnode._state, vnode, hooks) } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -412,8 +413,8 @@ module.exports = function($window) { result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && vnode.state.onbeforeremove) { - var result = vnode.state.onbeforeremove(vnode) + if (typeof vnode.tag !== "string" && vnode._state.onbeforeremove) { + var result = vnode._state.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -446,7 +447,7 @@ module.exports = function($window) { } function onremove(vnode) { if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && vnode.state.onremove) vnode.state.onremove(vnode) + if (typeof vnode.tag !== "string" && vnode._state.onremove) vnode._state.onremove.call(vnode.state, vnode) if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children @@ -585,7 +586,7 @@ module.exports = function($window) { 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) + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize diff --git a/render/vnode.js b/render/vnode.js index 13ed393f..ce137703 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) From 2d2c53fcfc020d57524dc5decc3b8852896f7e58 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Sun, 26 Mar 2017 23:01:04 +0200 Subject: [PATCH 4/9] Add some docs on undocumented vnode fields --- docs/vnodes.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/vnodes.md b/docs/vnodes.md index 317b9c47..cc6139bd 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -73,9 +73,11 @@ Property | Type | Description `text` | `(String|Number|Boolean)?` | This is used instead of `children` if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the `text` property even if they have a text node as their only child. `dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range. `domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property). -`state` | `Object`? | An object that is persisted between redraws. It is provided by the core engine when needed. In component vnodes, the `state` inherits prototypically from the component object/class. -`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use it. - +`state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In component vnodes, the `state` inherits prototypically from the component object/class. +`_state` | `Object?` | For components, a reference to the original `vnode.state` object, used to lookup the `view` and hooks. This property is only used internally by Mithril, do not use or modify it. +`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use or modify it. +`instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril, do not use or modify it. +`skip` | `Boolean` | This property is only used internally by Mithril when diffing keyed lists, do not use or modify it. --- From 32ada55e00628daa8d63cd589f381de83ab9169e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 27 Mar 2017 09:55:43 +0200 Subject: [PATCH 5/9] Make the state undefined, not null during class and closure component initialization --- render/render.js | 2 +- render/tests/test-component.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/render/render.js b/render/render.js index ce40b9d9..d07035c0 100644 --- a/render/render.js +++ b/render/render.js @@ -109,7 +109,7 @@ module.exports = function($window) { if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true } else { - vnode.state = null + vnode.state = void 0 sentinel = vnode.tag if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 8fccb9c3..2f59feef 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -894,7 +894,7 @@ o.spec("component", function() { o("Constructible", function() { var oninit = o.spy() var component = o.spy(function(vnode){ - o(vnode.state).equals(null) + o(vnode.state).equals(undefined) o(oninit.callCount).equals(0) }) var view = o.spy(function(){ @@ -920,7 +920,7 @@ o.spec("component", function() { return "" }) var component = o.spy(function(vnode) { - o(vnode.state).equals(null) + o(vnode.state).equals(undefined) o(oninit.callCount).equals(0) return state = { view: view From f7450c96b0d3e17b6b3823bdab8065984d378736 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 27 Mar 2017 10:38:59 +0200 Subject: [PATCH 6/9] Document closure components (formerly known as "factory components") and flesh out class components docs --- docs/components.md | 147 +++++++++++++++++++++++++++++++++------------ docs/vnodes.md | 2 +- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/docs/components.md b/docs/components.md index c67ea89b..940b7444 100644 --- a/docs/components.md +++ b/docs/components.md @@ -101,6 +101,109 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl --- +### Alternate component syntaxes + +#### ES6 classes + +Components can also be written using ES6 class syntax: + +```javascript +class ES6ClassComponent { + constructor(vnode) { + // vnode.state is undefined at this point + this.kind = "ES6 class" + } + view() { + return m("div", "Hello from an " + this.kind) + } + oncreate() { + console.log(`A ${this.kind} component was created`) + } +} +``` + +Component classes must define a `view()` method. + +They can be consumed in the same way regular components can. + +```javascript +// EXAMPLE: via m.render +m.render(document.body, m(ES6ClassComponent)) + +// EXAMPLE: via m.mount +m.mount(document.body, ES6ClassComponent) + +// EXAMPLE: via m.route +m.route(document.body, "/", { + "/": ES6ClassComponent +}) + +// EXAMPLE: component composition +class AnotherES6ClassComponent { + view() { + return m("main", [ + m(ES6ClassComponent) + ]) + } +} +``` + +More generally, a constructible function whose `.prototype.view` is a function will be treated as a class. + +#### Closure components + +Functionally minded developers may prefer using the "closure component" syntax: + +```javascript +function closureComponent(vnode) { + // vnode.state is undefined at this point + var kind = "closure component" + + return { + view: function() { + return m("div", "Hello from a " + kind) + }, + oncreate: function() { + console.log("We've created a " + kind) + } + } +} +``` + +The returned object must hold a `view` function. + +They can be consumed in the same way regular components can. + +```javascript +// EXAMPLE: via m.render +m.render(document.body, m(closureComponent)) + +// EXAMPLE: via m.mount +m.mount(document.body, closuresComponent) + +// EXAMPLE: via m.route +m.route(document.body, "/", { + "/": closureComponent +}) + +// EXAMPLE: component composition +function anotherClosureComponent() { + return { + view: function() { + return m("main", [ + m(closureComponent) + ]) + } + } +} +``` + +#### Mixing component kinds + +Components can be freely mixed. A Class component can have closure or POJO components as children, etc... + +--- + ### State Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns. @@ -109,7 +212,7 @@ The state of a component can be accessed three ways: as a blueprint at initializ #### At initialization -The component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple state initialization. +For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple state initialization. In the example below, `data` is a property of the `ComponentWithInitialState` component's state object. @@ -127,6 +230,10 @@ m(ComponentWithInitialState) //
Initial content
``` +For class components, the state is an instance of the class. + +For closure components, the state is the object returned by the closure. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead). + #### Via vnode.state State can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component. @@ -171,44 +278,6 @@ Be aware that when using ES5 functions, the value of `this` in nested anonymous --- -### ES6 classes - -Components can also be written using ES6 class syntax: - -```javascript -class ES6ClassComponent { - view() { - return m("div", "Hello from an ES6 class") - } -} -``` - -They can be consumed in the same way regular components can. - -```javascript -// EXAMPLE: via m.render -m.render(document.body, m(ES6ClassComponent)) - -// EXAMPLE: via m.mount -m.mount(document.body, ES6ClassComponent) - -// EXAMPLE: via m.route -m.route(document.body, "/", { - "/": ES6ClassComponent -}) - -// EXAMPLE: component composition -class AnotherES6ClassComponent { - view() { - return m("main", [ - m(ES6ClassComponent) - ]) - } -} -``` - ---- - ### Avoid anti-patterns Although Mithril is flexible, some code patterns are discouraged: diff --git a/docs/vnodes.md b/docs/vnodes.md index cc6139bd..cff0ebe4 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -73,7 +73,7 @@ Property | Type | Description `text` | `(String|Number|Boolean)?` | This is used instead of `children` if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the `text` property even if they have a text node as their only child. `dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range. `domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property). -`state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In component vnodes, the `state` inherits prototypically from the component object/class. +`state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the `state` inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure. `_state` | `Object?` | For components, a reference to the original `vnode.state` object, used to lookup the `view` and hooks. This property is only used internally by Mithril, do not use or modify it. `events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use or modify it. `instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril, do not use or modify it. From e20fd3f8764101afbdb3b18360820b7cc918ad48 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 27 Mar 2017 15:38:45 +0200 Subject: [PATCH 7/9] Only call onremove and onbeforeremove when they are functions. Fix #1706 --- render/render.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/render/render.js b/render/render.js index d07035c0..df26a1df 100644 --- a/render/render.js +++ b/render/render.js @@ -406,14 +406,14 @@ module.exports = function($window) { } function removeNode(vnode, context) { var expected = 1, called = 0 - if (vnode.attrs && vnode.attrs.onbeforeremove) { + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && vnode._state.onbeforeremove) { + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { var result = vnode._state.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ @@ -446,8 +446,8 @@ module.exports = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && vnode._state.onremove) vnode._state.onremove.call(vnode.state, vnode) + if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children From 0a767722834761801c07773cf1679f1e8d77dd1b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Gerardy Date: Mon, 27 Mar 2017 18:27:21 +0200 Subject: [PATCH 8/9] Add tests for components that overwrite their state in `oninit` --- render/tests/test-component.js | 91 ++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 2f59feef..94863236 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -764,6 +764,97 @@ o.spec("component", function() { o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) }) }) + o("lifecycle timing megatest (for a single component with the state overwritten)", function() { + var methods = { + view: o.spy(function(vnode) { + o(vnode.state).equals(1) + 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(vnode) { + o(vnode.state).equals(1) + o(attrs[hook].callCount).equals(methods[hook].callCount + 1) + }) + methods[hook] = o.spy(function(vnode) { + o(vnode.state).equals(1) + o(attrs[hook].callCount).equals(methods[hook].callCount) + }) + }) + + var attrsOninit = attrs.oninit + var methodsOninit = methods.oninit + attrs.oninit = o.spy(function(vnode){ + vnode.state = 1 + return attrsOninit.call(this, vnode) + }) + methods.oninit = o.spy(function(vnode){ + vnode.state = 1 + return methodsOninit.call(this, vnode) + }) + + 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) { From 40637fe99fe24cbd6d26e2d586687831c65ade04 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Mon, 27 Mar 2017 12:41:45 -0400 Subject: [PATCH 9/9] Update components.md [ci skip] --- docs/components.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/components.md b/docs/components.md index 940b7444..3d958aad 100644 --- a/docs/components.md +++ b/docs/components.md @@ -114,7 +114,7 @@ class ES6ClassComponent { this.kind = "ES6 class" } view() { - return m("div", "Hello from an " + this.kind) + return m("div", `Hello from an ${this.kind}`) } oncreate() { console.log(`A ${this.kind} component was created`) @@ -122,7 +122,7 @@ class ES6ClassComponent { } ``` -Component classes must define a `view()` method. +Component classes must define a `view()` method, detected via `.prototype.view`, to get the tree to render. They can be consumed in the same way regular components can. @@ -148,8 +148,6 @@ class AnotherES6ClassComponent { } ``` -More generally, a constructible function whose `.prototype.view` is a function will be treated as a class. - #### Closure components Functionally minded developers may prefer using the "closure component" syntax: @@ -170,7 +168,7 @@ function closureComponent(vnode) { } ``` -The returned object must hold a `view` function. +The returned object must hold a `view` function, used to get the tree to render. They can be consumed in the same way regular components can. @@ -230,9 +228,9 @@ m(ComponentWithInitialState) //
Initial content
``` -For class components, the state is an instance of the class. +For class components, the state is an instance of the class, set right after the constructor is called. -For closure components, the state is the object returned by the closure. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead). +For closure components, the state is the object returned by the closure, set right after the closure returns. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead). #### Via vnode.state