Support classes and factories as components (#1339)
* Support classes and factories as components * Tests for class and factory component support
This commit is contained in:
parent
ff16c7f47a
commit
3f3af74dde
5 changed files with 301 additions and 16 deletions
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue