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/test-component.js b/render/tests/test-component.js index 64cd6fcb..0ff4e07e 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -656,6 +656,149 @@ 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){ + 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() { @@ -675,7 +818,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 +841,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 +863,52 @@ 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) + 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) + 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) }) }) }) 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