From 3f3af74dde47a2c538944ac6b8c29a4056967a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 15 Feb 2017 04:48:02 +0100 Subject: [PATCH] Support classes and factories as components (#1339) * Support classes and factories as components * Tests for class and factory component support --- render/hyperscript.js | 2 +- render/render.js | 38 ++++++--- render/tests/test-component.js | 138 +++++++++++++++++++++++++++++++ render/tests/test-hyperscript.js | 15 +++- render/tests/test-render.js | 124 ++++++++++++++++++++++++++- 5 files changed, 301 insertions(+), 16 deletions(-) diff --git a/render/hyperscript.js b/render/hyperscript.js index a59bba45..1630afce 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -5,7 +5,7 @@ var Vnode = require("../render/vnode") var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorCache = {} function hyperscript(selector) { - if (selector == null || typeof selector !== "string" && typeof selector.view !== "function") { + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { throw Error("The selector must be either a string or a component."); } diff --git a/render/render.js b/render/render.js index 0f2936d4..3ed4ed2a 100644 --- a/render/render.js +++ b/render/render.js @@ -101,13 +101,25 @@ module.exports = function($window) { return element } function createComponent(parent, vnode, hooks, ns, nextSibling) { - vnode.state = Object.create(vnode.tag) - var view = vnode.tag.view - if (view.reentrantLock != null) return $emptyFragment - view.reentrantLock = true - initLifecycle(vnode.tag, vnode, hooks) - vnode.instance = Vnode.normalize(view.call(vnode.state, vnode)) - view.reentrantLock = null + var sentinel + if (typeof vnode.tag === "function") { + 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 + } + + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + sentinel.$$reentrantLock$$ = null + 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) @@ -294,8 +306,8 @@ module.exports = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.tag.view.call(vnode.state, vnode)) - updateLifecycle(vnode.tag, vnode, hooks, recycling) + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + updateLifecycle(vnode.state, vnode, hooks, recycling) 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) @@ -387,8 +399,8 @@ module.exports = function($window) { result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) { - var result = vnode.tag.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && vnode.state.onbeforeremove) { + var result = vnode.state.onbeforeremove(vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -421,7 +433,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.tag.onremove) vnode.tag.onremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && vnode.state.onremove) vnode.state.onremove(vnode) if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children @@ -556,7 +568,7 @@ module.exports = function($window) { function shouldUpdate(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.tag.onbeforeupdate === "function") forceComponentUpdate = vnode.tag.onbeforeupdate.call(vnode.state, vnode, old) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") forceComponentUpdate = vnode.state.onbeforeupdate(vnode, old) if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 4bb0a3b6..ed70fe7d 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -9,6 +9,7 @@ o.spec("component", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") + render = vdom($window).render }) @@ -694,4 +695,141 @@ o.spec("component", function() { } }) }) + 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 "" + }) + } + } + + 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) + }) + }) }) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 32e767d8..9e554541 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -424,7 +424,7 @@ o.spec("hyperscript", function() { }) }) o.spec("components", function() { - o("works", function() { + o("works with POJOs", function() { var component = { view: function() { return m("div") @@ -432,6 +432,19 @@ o.spec("hyperscript", function() { } var vnode = m(component, {id: "a"}, "b") + o(vnode.tag).equals(component) + o(vnode.attrs.id).equals("a") + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("b") + }) + o("works with functions", function() { + var component = o.spy() + + var vnode = m(component, {id: "a"}, "b") + + o(component.callCount).equals(0) + o(vnode.tag).equals(component) o(vnode.attrs.id).equals("a") o(vnode.children.length).equals(1) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 515fe885..2f3ebb9a 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -32,7 +32,7 @@ o.spec("render", function() { o(threw).equals(true) }) - o("does not enter infinite loop when oninit triggers render and view throws", function(done) { + o("does not enter infinite loop when oninit triggers render and view throws with an object literal component", function(done) { var A = { oninit: init, view: function() {throw new Error("error")} @@ -55,6 +55,128 @@ o.spec("render", function() { o(threwOuter).equals(true) }) + o("does not try to re-initialize a constructibe component whose view has thrown", function() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A(){} + A.prototype.view = function() {throw new Error("error")} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a constructible component whose oninit has thrown", function() { + var oninit = o.spy(function(){throw new Error("error")}) + var onbeforeupdate = o.spy() + function A(){} + A.prototype.view = function(){} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a constructible component whose constructor has thrown", function() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A(){throw new Error("error")} + A.prototype.view = function() {} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(0) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + 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() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A() { + return { + view: function(vnode) {throw new Error("error")}, + oninit: oninit, + onbeforeupdate: onbeforeupdate + } + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + 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() { + var oninit = o.spy(function(vnode) {throw new Error("error")}) + var onbeforeupdate = o.spy() + function A() { + return { + view: function(vnode) {}, + oninit: oninit, + onbeforeupdate: onbeforeupdate + } + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + 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() { + function A() { + throw new Error("error") + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + }) o("lifecycle methods work in keyed children of recycled keyed", function() { var createA = o.spy() var updateA = o.spy()