Fix keys, normalize holes (#2452)

* Fix #2434

* Treat holes as unkeyed, normalize boolean/null/undefined

This brings a lot better consistency with that API, even though it's
slightly breaking. (I had to update a bunch of tests to correspond with
it.)

* Fill in PR number [skip ci]
This commit is contained in:
Isiah Meadows 2019-07-03 17:05:44 -04:00 committed by GitHub
parent 86d64e213f
commit 6c562d2b9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 182 deletions

View file

@ -45,6 +45,15 @@
- Previously, it was only set for all non-`GET` methods and only when `useBody: true` was passed (the default), and it was always set for them. Now it's automatically omitted when no body is present, so the hole is slightly broadened. - Previously, it was only set for all non-`GET` methods and only when `useBody: true` was passed (the default), and it was always set for them. Now it's automatically omitted when no body is present, so the hole is slightly broadened.
- route: query parameters in hash strings are no longer supported ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows)) - route: query parameters in hash strings are no longer supported ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows))
- It's technically invalid in hashes, so I'd rather push people to keep in line with spec. - It's technically invalid in hashes, so I'd rather push people to keep in line with spec.
- render: validate all elements are either keyed or unkeyed, and treat `null`/`undefined`/booleans as strictly unkeyed ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
- Gives a nice little perf boost with keyed fragments.
- Minor, but imperceptible impact (within the margin of error) with unkeyed fragments.
- Also makes the model a lot more consistent - all values are either keyed or unkeyed.
- vnodes: normalize boolean children to `null`/`undefined` at the vnode level, always stringify non-object children that aren't holes ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
- Previously, `true` was equivalent to `"true"` and `false` was equivalent to `""`.
- Previously, numeric children weren't coerced. Now, they are.
- Unlikely to break most components, but it *could* break some users.
- This increases consistency with how booleans are handled with children, so it should be more intuitive.
#### News #### News
@ -101,6 +110,7 @@
- route: arbitrary prefixes are properly supported now, including odd prefixes like `?#` and invalid prefixes like `#foo#bar` ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows)) - route: arbitrary prefixes are properly supported now, including odd prefixes like `?#` and invalid prefixes like `#foo#bar` ([#2448](https://github.com/MithrilJS/mithril.js/pull/2448) [@isiahmeadows](https://github.com/isiahmeadows))
- request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@isiahmeadows](https://github.com/isiahmeadows)) - request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@isiahmeadows](https://github.com/isiahmeadows))
- render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@isiahmeadows](https://github.com/isiahmeadows)) - render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@isiahmeadows](https://github.com/isiahmeadows))
- docs: clarify valid key usage ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
--- ---

View file

@ -159,7 +159,7 @@ m("button", {
If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent. If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent.
If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null`or `undefined`, it is ignored. If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null` or `undefined`, it is ignored.
If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`. If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`.

View file

@ -75,6 +75,27 @@ function correctUserList(users) {
} }
``` ```
Also, you might want to reinitialize a component. You can use the common pattern of a single-item keyed fragment where you change the key to destroy and reinitialize the element.
```javascript
function ResettableToggle() {
var toggleKey = false
function reset() {
toggleKey = !toggleKey
}
return {
view: function() {
return [
m("button", {onclick: reset}, "Reset toggle"),
[m(Toggle, {key: toggleKey})]
]
}
}
}
```
--- ---
### Debugging key related issues ### Debugging key related issues
@ -178,6 +199,10 @@ m("div", [
]) ])
``` ```
In fact, this will cause an error to be thrown, to remind you to not do it. So don't do it!
Note that `null`s, `undefined`s, and booleans are considered unkeyed nodes. If you want the keyed equivalent, use `m.fragment({key: ...}, [])`, a keyed empty fragment.
#### Avoid passing model data directly to components if the model uses `key` as a data property #### 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-initialize, 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: 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-initialize, 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:

View file

@ -44,8 +44,24 @@ module.exports = function($window) {
return null return null
} }
} }
function validateKeys(vnodes, isKeyed) {
// Note: this is a *very* perf-sensitive check.
// Fun fact: merging the loop like this is somehow faster than splitting
// it, noticeably so.
for (var i = 1; i < vnodes.length; i++) {
if ((vnodes[i] != null && vnodes[i].key != null) !== isKeyed) {
throw new TypeError("Vnodes must either always have keys or never have keys!")
}
}
}
//create //create
function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { function createNodesChecked(parent, vnodes, hooks, nextSibling, ns) {
if (vnodes.length) {
validateKeys(vnodes, vnodes[0] != null && vnodes[0].key != null)
createNodesUnchecked(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
}
}
function createNodesUnchecked(parent, vnodes, start, end, hooks, nextSibling, ns) {
for (var i = start; i < end; i++) { for (var i = start; i < end; i++) {
var vnode = vnodes[i] var vnode = vnodes[i]
if (vnode != null) { if (vnode != null) {
@ -99,7 +115,7 @@ module.exports = function($window) {
var fragment = $doc.createDocumentFragment() var fragment = $doc.createDocumentFragment()
if (vnode.children != null) { if (vnode.children != null) {
var children = vnode.children var children = vnode.children
createNodes(fragment, children, 0, children.length, hooks, null, ns) createNodesChecked(fragment, children, hooks, null, ns)
} }
vnode.dom = fragment.firstChild vnode.dom = fragment.firstChild
vnode.domSize = fragment.childNodes.length vnode.domSize = fragment.childNodes.length
@ -130,7 +146,7 @@ module.exports = function($window) {
} }
if (vnode.children != null) { if (vnode.children != null) {
var children = vnode.children var children = vnode.children
createNodes(element, children, 0, children.length, hooks, null, ns) createNodesChecked(element, children, hooks, null, ns)
if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs) if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs)
} }
} }
@ -273,26 +289,19 @@ module.exports = function($window) {
function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
if (old === vnodes || old == null && vnodes == null) return if (old === vnodes || old == null && vnodes == null) return
else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) else if (old == null || old.length === 0) createNodesChecked(parent, vnodes, hooks, nextSibling, ns)
else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length) else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length)
else { else {
var start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null var isOldKeyed = old[0] != null && old[0].key != null
for (; oldStart < old.length; oldStart++) { var isKeyed = vnodes[0] != null && vnodes[0].key != null
if (old[oldStart] != null) { var start = 0, oldStart = 0
isOldKeyed = old[oldStart].key != null validateKeys(vnodes, isKeyed)
break if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++
} if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++
}
for (; start < vnodes.length; start++) {
if (vnodes[start] != null) {
isKeyed = vnodes[start].key != null
break
}
}
if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls
if (isOldKeyed !== isKeyed) { if (isOldKeyed !== isKeyed) {
removeNodes(old, oldStart, old.length) removeNodes(old, oldStart, old.length)
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) createNodesUnchecked(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
} else if (!isKeyed) { } else if (!isKeyed) {
// Don't index past the end of either list (causes deopts). // Don't index past the end of either list (causes deopts).
var commonLength = old.length < vnodes.length ? old.length : vnodes.length var commonLength = old.length < vnodes.length ? old.length : vnodes.length
@ -309,7 +318,7 @@ module.exports = function($window) {
else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns)
} }
if (old.length > commonLength) removeNodes(old, start, old.length) if (old.length > commonLength) removeNodes(old, start, old.length)
if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) if (vnodes.length > commonLength) createNodesUnchecked(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
} else { } else {
// keyed diff // keyed diff
var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling
@ -318,37 +327,22 @@ module.exports = function($window) {
while (oldEnd >= oldStart && end >= start) { while (oldEnd >= oldStart && end >= start) {
oe = old[oldEnd] oe = old[oldEnd]
ve = vnodes[end] ve = vnodes[end]
if (oe == null) oldEnd-- if (oe.key !== ve.key) break
else if (ve == null) end--
else if (oe.key === ve.key) {
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom if (ve.dom != null) nextSibling = ve.dom
oldEnd--, end-- oldEnd--, end--
} else {
break
}
} }
// top-down // top-down
while (oldEnd >= oldStart && end >= start) { while (oldEnd >= oldStart && end >= start) {
o = old[oldStart] o = old[oldStart]
v = vnodes[start] v = vnodes[start]
if (o == null) oldStart++ if (o.key !== v.key) break
else if (v == null) start++
else if (o.key === v.key) {
oldStart++, start++ oldStart++, start++
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
} else {
break
}
} }
// swaps and list reversals // swaps and list reversals
while (oldEnd >= oldStart && end >= start) { while (oldEnd >= oldStart && end >= start) {
if (o == null) oldStart++ if (start === end) break
else if (v == null) start++
else if (oe == null) oldEnd--
else if (ve == null) end--
else if (start === end) break
else {
if (o.key !== ve.key || oe.key !== v.key) break if (o.key !== ve.key || oe.key !== v.key) break
topSibling = getNextSibling(old, oldStart, nextSibling) topSibling = getNextSibling(old, oldStart, nextSibling)
insertNode(parent, toFragment(oe), topSibling) insertNode(parent, toFragment(oe), topSibling)
@ -357,7 +351,6 @@ module.exports = function($window) {
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns) if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom if (ve.dom != null) nextSibling = ve.dom
oldStart++; oldEnd-- oldStart++; oldEnd--
}
oe = old[oldEnd] oe = old[oldEnd]
ve = vnodes[end] ve = vnodes[end]
o = old[oldStart] o = old[oldStart]
@ -365,20 +358,15 @@ module.exports = function($window) {
} }
// bottom up once again // bottom up once again
while (oldEnd >= oldStart && end >= start) { while (oldEnd >= oldStart && end >= start) {
if (oe == null) oldEnd-- if (oe.key !== ve.key) break
else if (ve == null) end--
else if (oe.key === ve.key) {
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom if (ve.dom != null) nextSibling = ve.dom
oldEnd--, end-- oldEnd--, end--
} else {
break
}
oe = old[oldEnd] oe = old[oldEnd]
ve = vnodes[end] ve = vnodes[end]
} }
if (start > end) removeNodes(old, oldStart, oldEnd + 1) if (start > end) removeNodes(old, oldStart, oldEnd + 1)
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) else if (oldStart > oldEnd) createNodesUnchecked(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
else { else {
// inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices
@ -386,7 +374,6 @@ module.exports = function($window) {
for (i = end; i >= start; i--) { for (i = end; i >= start; i--) {
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1) if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
ve = vnodes[i] ve = vnodes[i]
if (ve != null) {
var oldIndex = map[ve.key] var oldIndex = map[ve.key]
if (oldIndex != null) { if (oldIndex != null) {
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
@ -398,10 +385,9 @@ module.exports = function($window) {
matched++ matched++
} }
} }
}
nextSibling = originalNextSibling nextSibling = originalNextSibling
if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1) if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1)
if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) if (matched === 0) createNodesUnchecked(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
else { else {
if (pos === -1) { if (pos === -1) {
// the indices of the indices of the items that are part of the // the indices of the indices of the items that are part of the
@ -541,26 +527,26 @@ module.exports = function($window) {
// occur multiple times) and returns an array with the indices // occur multiple times) and returns an array with the indices
// of the items that are part of the longest increasing // of the items that are part of the longest increasing
// subsequece // subsequece
var lisTemp = []
function makeLisIndices(a) { function makeLisIndices(a) {
var p = a.slice() var result = [0]
var result = [] var u = 0, v = 0, i = 0
result.push(0) var il = lisTemp.length = a.length
var u for (var i = 0; i < il; i++) lisTemp[i] = a[i]
var v for (var i = 0; i < il; ++i) {
for (var i = 0, il = a.length; i < il; ++i) { if (a[i] === -1) continue
if (a[i] === -1) {
continue
}
var j = result[result.length - 1] var j = result[result.length - 1]
if (a[j] < a[i]) { if (a[j] < a[i]) {
p[i] = j lisTemp[i] = j
result.push(i) result.push(i)
continue continue
} }
u = 0 u = 0
v = result.length - 1 v = result.length - 1
while (u < v) { while (u < v) {
var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise // Fast integer average without overflow.
// eslint-disable-next-line no-bitwise
var c = (u >>> 1) + (v >>> 1) + (u & v & 1)
if (a[result[c]] < a[i]) { if (a[result[c]] < a[i]) {
u = c + 1 u = c + 1
} }
@ -569,9 +555,7 @@ module.exports = function($window) {
} }
} }
if (a[i] < a[result[u]]) { if (a[i] < a[result[u]]) {
if (u > 0) { if (u > 0) lisTemp[i] = result[u - 1]
p[i] = result[u - 1]
}
result[u] = i result[u] = i
} }
} }
@ -579,8 +563,9 @@ module.exports = function($window) {
v = result[u - 1] v = result[u - 1]
while (u-- > 0) { while (u-- > 0) {
result[u] = v result[u] = v
v = p[v] v = lisTemp[v]
} }
lisTemp.length = 0
return result return result
} }

View file

@ -110,7 +110,7 @@ o.spec("component", function() {
visible = false visible = false
render(root, [{tag: component}]) render(root, [{tag: component}])
o(root.firstChild.nodeValue).equals("") o(root.childNodes.length).equals(0)
}) })
o("updates root from null to null", function() { o("updates root from null to null", function() {
var component = createComponent({ var component = createComponent({
@ -218,7 +218,7 @@ o.spec("component", function() {
o(root.firstChild.nodeType).equals(3) o(root.firstChild.nodeType).equals(3)
o(root.firstChild.nodeValue).equals("0") o(root.firstChild.nodeValue).equals("0")
}) })
o("can return boolean", function() { o("can return `true`", function() {
var component = createComponent({ var component = createComponent({
view: function() { view: function() {
return true return true
@ -226,10 +226,9 @@ o.spec("component", function() {
}) })
render(root, [{tag: component}]) render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3) o(root.childNodes.length).equals(0)
o(root.firstChild.nodeValue).equals("true")
}) })
o("can return falsy boolean", function() { o("can return `false`", function() {
var component = createComponent({ var component = createComponent({
view: function() { view: function() {
return false return false
@ -237,8 +236,7 @@ o.spec("component", function() {
}) })
render(root, [{tag: component}]) render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3) o(root.childNodes.length).equals(0)
o(root.firstChild.nodeValue).equals("")
}) })
o("can return null", function() { o("can return null", function() {
var component = createComponent({ var component = createComponent({
@ -293,8 +291,7 @@ o.spec("component", function() {
}) })
render(root, [{tag: component}]) render(root, [{tag: component}])
o(root.firstChild.nodeType).equals(3) o(root.childNodes.length).equals(0)
o(root.firstChild.nodeValue).equals("")
try { try {
render(root, [{tag: component}]) render(root, [{tag: component}])

View file

@ -57,25 +57,23 @@ function runTest(name, fragment) {
var vnode = fragment([1]) var vnode = fragment([1])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(1) o(vnode.children[0].children).equals("1")
}) })
o("handles falsy number single child", function() { o("handles falsy number single child", function() {
var vnode = fragment([0]) var vnode = fragment([0])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
}) })
o("handles boolean single child", function() { o("handles boolean single child", function() {
var vnode = fragment([true]) var vnode = fragment([true])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null])
o(vnode.children[0].children).equals(true)
}) })
o("handles falsy boolean single child", function() { o("handles falsy boolean single child", function() {
var vnode = fragment([false]) var vnode = fragment([false])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null])
o(vnode.children[0].children).equals("")
}) })
o("handles null single child", function() { o("handles null single child", function() {
var vnode = fragment([null]) var vnode = fragment([null])
@ -85,7 +83,7 @@ function runTest(name, fragment) {
o("handles undefined single child", function() { o("handles undefined single child", function() {
var vnode = fragment([undefined]) var vnode = fragment([undefined])
o(vnode.children[0]).equals(undefined) o(vnode.children).deepEquals([null])
}) })
o("handles multiple string children", function() { o("handles multiple string children", function() {
var vnode = fragment(["", "a"]) var vnode = fragment(["", "a"])
@ -99,29 +97,25 @@ function runTest(name, fragment) {
var vnode = fragment([0, 1]) var vnode = fragment([0, 1])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
o(vnode.children[1].tag).equals("#") o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(1) o(vnode.children[1].children).equals("1")
}) })
o("handles multiple boolean children", function() { o("handles multiple boolean children", function() {
var vnode = fragment([false, true]) var vnode = fragment([false, true])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null, null])
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(true)
}) })
o("handles multiple null/undefined child", function() { o("handles multiple null/undefined child", function() {
var vnode = fragment([null, undefined]) var vnode = fragment([null, undefined])
o(vnode.children[0]).equals(null) o(vnode.children).deepEquals([null, null])
o(vnode.children[1]).equals(undefined)
}) })
o("handles falsy number single child without attrs", function() { o("handles falsy number single child without attrs", function() {
var vnode = fragment(0) var vnode = fragment(0)
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
}) })
}) })
o.spec("children with attrs", function() { o.spec("children with attrs", function() {
@ -141,35 +135,33 @@ function runTest(name, fragment) {
var vnode = fragment({}, [1]) var vnode = fragment({}, [1])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(1) o(vnode.children[0].children).equals("1")
}) })
o("handles falsy number single child", function() { o("handles falsy number single child", function() {
var vnode = fragment({}, [0]) var vnode = fragment({}, [0])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
}) })
o("handles boolean single child", function() { o("handles boolean single child", function() {
var vnode = fragment({}, [true]) var vnode = fragment({}, [true])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null])
o(vnode.children[0].children).equals(true)
}) })
o("handles falsy boolean single child", function() { o("handles falsy boolean single child", function() {
var vnode = fragment({}, [false]) var vnode = fragment({}, [false])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null])
o(vnode.children[0].children).equals("")
}) })
o("handles null single child", function() { o("handles null single child", function() {
var vnode = fragment({}, [null]) var vnode = fragment({}, [null])
o(vnode.children[0]).equals(null) o(vnode.children).deepEquals([null])
}) })
o("handles undefined single child", function() { o("handles undefined single child", function() {
var vnode = fragment({}, [undefined]) var vnode = fragment({}, [undefined])
o(vnode.children[0]).equals(undefined) o(vnode.children).deepEquals([null])
}) })
o("handles multiple string children", function() { o("handles multiple string children", function() {
var vnode = fragment({}, ["", "a"]) var vnode = fragment({}, ["", "a"])
@ -183,23 +175,19 @@ function runTest(name, fragment) {
var vnode = fragment({}, [0, 1]) var vnode = fragment({}, [0, 1])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
o(vnode.children[1].tag).equals("#") o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(1) o(vnode.children[1].children).equals("1")
}) })
o("handles multiple boolean children", function() { o("handles multiple boolean children", function() {
var vnode = fragment({}, [false, true]) var vnode = fragment({}, [false, true])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null, null])
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(true)
}) })
o("handles multiple null/undefined child", function() { o("handles multiple null/undefined child", function() {
var vnode = fragment({}, [null, undefined]) var vnode = fragment({}, [null, undefined])
o(vnode.children[0]).equals(null) o(vnode.children).deepEquals([null, null])
o(vnode.children[1]).equals(undefined)
}) })
}) })
}) })

View file

@ -382,32 +382,32 @@ o.spec("hyperscript", function() {
o("handles number single child", function() { o("handles number single child", function() {
var vnode = m("div", {}, [1]) var vnode = m("div", {}, [1])
o(vnode.text).equals(1) o(vnode.text).equals("1")
}) })
o("handles falsy number single child", function() { o("handles falsy number single child", function() {
var vnode = m("div", {}, [0]) var vnode = m("div", {}, [0])
o(vnode.text).equals(0) o(vnode.text).equals("0")
}) })
o("handles boolean single child", function() { o("handles boolean single child", function() {
var vnode = m("div", {}, [true]) var vnode = m("div", {}, [true])
o(vnode.text).equals(true) o(vnode.children).deepEquals([null])
}) })
o("handles falsy boolean single child", function() { o("handles falsy boolean single child", function() {
var vnode = m("div", {}, [false]) var vnode = m("div", {}, [false])
o(vnode.text).equals("") o(vnode.children).deepEquals([null])
}) })
o("handles null single child", function() { o("handles null single child", function() {
var vnode = m("div", {}, [null]) var vnode = m("div", {}, [null])
o(vnode.children[0]).equals(null) o(vnode.children).deepEquals([null])
}) })
o("handles undefined single child", function() { o("handles undefined single child", function() {
var vnode = m("div", {}, [undefined]) var vnode = m("div", {}, [undefined])
o(vnode.children[0]).equals(undefined) o(vnode.children).deepEquals([null])
}) })
o("handles multiple string children", function() { o("handles multiple string children", function() {
var vnode = m("div", {}, ["", "a"]) var vnode = m("div", {}, ["", "a"])
@ -421,28 +421,24 @@ o.spec("hyperscript", function() {
var vnode = m("div", {}, [0, 1]) var vnode = m("div", {}, [0, 1])
o(vnode.children[0].tag).equals("#") o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0) o(vnode.children[0].children).equals("0")
o(vnode.children[1].tag).equals("#") o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(1) o(vnode.children[1].children).equals("1")
}) })
o("handles multiple boolean children", function() { o("handles multiple boolean children", function() {
var vnode = m("div", {}, [false, true]) var vnode = m("div", {}, [false, true])
o(vnode.children[0].tag).equals("#") o(vnode.children).deepEquals([null, null])
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(true)
}) })
o("handles multiple null/undefined child", function() { o("handles multiple null/undefined child", function() {
var vnode = m("div", {}, [null, undefined]) var vnode = m("div", {}, [null, undefined])
o(vnode.children[0]).equals(null) o(vnode.children).deepEquals([null, null])
o(vnode.children[1]).equals(undefined)
}) })
o("handles falsy number single child without attrs", function() { o("handles falsy number single child without attrs", function() {
var vnode = m("div", 0) var vnode = m("div", 0)
o(vnode.text).equals(0) o(vnode.text).equals("0")
}) })
}) })
o.spec("permutations", function() { o.spec("permutations", function() {
@ -511,31 +507,31 @@ o.spec("hyperscript", function() {
var vnode = m("div", {a: "b"}, [1]) var vnode = m("div", {a: "b"}, [1])
o(vnode.attrs.a).equals("b") o(vnode.attrs.a).equals("b")
o(vnode.text).equals(1) o(vnode.text).equals("1")
}) })
o("handles attr and single falsy number text child", function() { o("handles attr and single falsy number text child", function() {
var vnode = m("div", {a: "b"}, [0]) var vnode = m("div", {a: "b"}, [0])
o(vnode.attrs.a).equals("b") o(vnode.attrs.a).equals("b")
o(vnode.text).equals(0) o(vnode.text).equals("0")
}) })
o("handles attr and single boolean text child", function() { o("handles attr and single boolean text child", function() {
var vnode = m("div", {a: "b"}, [true]) var vnode = m("div", {a: "b"}, [true])
o(vnode.attrs.a).equals("b") o(vnode.attrs.a).equals("b")
o(vnode.text).equals(true) o(vnode.children).deepEquals([null])
}) })
o("handles attr and single falsy boolean text child", function() { o("handles attr and single falsy boolean text child", function() {
var vnode = m("div", {a: "b"}, [0]) var vnode = m("div", {a: "b"}, [0])
o(vnode.attrs.a).equals("b") o(vnode.attrs.a).equals("b")
o(vnode.text).equals(0) o(vnode.text).equals("0")
}) })
o("handles attr and single false boolean text child", function() { o("handles attr and single false boolean text child", function() {
var vnode = m("div", {a: "b"}, [false]) var vnode = m("div", {a: "b"}, [false])
o(vnode.attrs.a).equals("b") o(vnode.attrs.a).equals("b")
o(vnode.text).equals("") o(vnode.children).deepEquals([null])
}) })
o("handles attr and single text child unwrapped", function() { o("handles attr and single text child unwrapped", function() {
var vnode = m("div", {a: "b"}, "c") var vnode = m("div", {a: "b"}, "c")

View file

@ -34,24 +34,22 @@ o.spec("normalize", function() {
var node = Vnode.normalize(1) var node = Vnode.normalize(1)
o(node.tag).equals("#") o(node.tag).equals("#")
o(node.children).equals(1) o(node.children).equals("1")
}) })
o("normalizes falsy number into text node", function() { o("normalizes falsy number into text node", function() {
var node = Vnode.normalize(0) var node = Vnode.normalize(0)
o(node.tag).equals("#") o(node.tag).equals("#")
o(node.children).equals(0) o(node.children).equals("0")
}) })
o("normalizes boolean into text node", function() { o("normalizes `true` to `null`", function() {
var node = Vnode.normalize(true) var node = Vnode.normalize(true)
o(node.tag).equals("#") o(node).equals(null)
o(node.children).equals(true)
}) })
o("normalizes falsy boolean into empty text node", function() { o("normalizes `false` to `null`", function() {
var node = Vnode.normalize(false) var node = Vnode.normalize(false)
o(node.tag).equals("#") o(node).equals(null)
o(node.children).equals("")
}) })
}) })

View file

@ -16,10 +16,9 @@ o.spec("normalizeChildren", function() {
o(children[0].tag).equals("#") o(children[0].tag).equals("#")
o(children[0].children).equals("a") o(children[0].children).equals("a")
}) })
o("normalizes `false` values into empty string text nodes", function() { o("normalizes `false` values into `null`s", function() {
var children = Vnode.normalizeChildren([false]) var children = Vnode.normalizeChildren([false])
o(children[0].tag).equals("#") o(children[0]).equals(null)
o(children[0].children).equals("")
}) })
}) })

View file

@ -451,9 +451,9 @@ o.spec("updateNodes", function() {
o(updated[1].dom.nodeName).equals("I") o(updated[1].dom.nodeName).equals("I")
o(updated[1].dom).equals(root.childNodes[2]) o(updated[1].dom).equals(root.childNodes[2])
}) })
o("populates array followed by null then el", function() { o("populates array followed by el keyed", function() {
var vnodes = [{tag: "[", key: 1, children: []}, null, {tag: "i", key: 2}] var vnodes = [{tag: "[", key: 1, children: []}, {tag: "i", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}] var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, {tag: "i", key: 2}]
render(root, vnodes) render(root, vnodes)
render(root, updated) render(root, updated)
@ -464,10 +464,38 @@ o.spec("updateNodes", function() {
o(updated[0].domSize).equals(2) o(updated[0].domSize).equals(2)
o(updated[0].dom.nextSibling.nodeName).equals("B") o(updated[0].dom.nextSibling.nodeName).equals("B")
o(updated[0].dom.nextSibling).equals(root.childNodes[1]) o(updated[0].dom.nextSibling).equals(root.childNodes[1])
o(updated[2].dom.nodeName).equals("I") o(updated[1].dom.nodeName).equals("I")
o(updated[2].dom).equals(root.childNodes[2]) o(updated[1].dom).equals(root.childNodes[2])
}) })
o("populates childless array followed by el", function() { o("throws populates array followed by el keyed", function() {
var vnodes = [{tag: "[", key: 1, children: []}, {tag: "i", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, {tag: "i", key: 2}]
render(root, vnodes)
render(root, updated)
o(root.childNodes.length).equals(3)
o(updated[0].dom.nodeName).equals("A")
o(updated[0].dom).equals(root.childNodes[0])
o(updated[0].domSize).equals(2)
o(updated[0].dom.nextSibling.nodeName).equals("B")
o(updated[0].dom.nextSibling).equals(root.childNodes[1])
o(updated[1].dom.nodeName).equals("I")
o(updated[1].dom).equals(root.childNodes[2])
})
o("throws if array followed by null then el on first render keyed", function() {
var vnodes = [{tag: "[", key: 1, children: []}, null, {tag: "i", key: 2}]
o(function () { render(root, vnodes) }).throws(TypeError)
})
o("throws if array followed by null then el on next render keyed", function() {
var vnodes = [{tag: "[", key: 1, children: []}, {tag: "i", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}]
render(root, vnodes)
o(function () { render(root, updated) }).throws(TypeError)
})
o("populates childless array replaced followed by el keyed", function() {
var vnodes = [{tag: "[", key: 1}, {tag: "i", key: 2}] var vnodes = [{tag: "[", key: 1}, {tag: "i", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, {tag: "i", key: 2}] var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, {tag: "i", key: 2}]
@ -483,21 +511,12 @@ o.spec("updateNodes", function() {
o(updated[1].dom.nodeName).equals("I") o(updated[1].dom.nodeName).equals("I")
o(updated[1].dom).equals(root.childNodes[2]) o(updated[1].dom).equals(root.childNodes[2])
}) })
o("populates childless array followed by null then el", function() { o("throws if childless array replaced followed by null then el keyed", function() {
var vnodes = [{tag: "[", key: 1}, null, {tag: "i", key: 2}] var vnodes = [{tag: "[", key: 1}, {tag: "i", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}] var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}]
render(root, vnodes) render(root, vnodes)
render(root, updated) o(function () { render(root, updated) }).throws(TypeError)
o(root.childNodes.length).equals(3)
o(updated[0].dom.nodeName).equals("A")
o(updated[0].dom).equals(root.childNodes[0])
o(updated[0].domSize).equals(2)
o(updated[0].dom.nextSibling.nodeName).equals("B")
o(updated[0].dom.nextSibling).equals(root.childNodes[1])
o(updated[2].dom.nodeName).equals("I")
o(updated[2].dom).equals(root.childNodes[2])
}) })
o("moves from end to start", function() { o("moves from end to start", function() {
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}]

View file

@ -5,8 +5,9 @@ function Vnode(tag, key, attrs, children, text, dom) {
} }
Vnode.normalize = function(node) { Vnode.normalize = function(node) {
if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined) if (node == null || typeof node === "boolean") return null
return node if (typeof node === "object") return node
return Vnode("#", undefined, undefined, String(node), undefined, undefined)
} }
Vnode.normalizeChildren = function(input) { Vnode.normalizeChildren = function(input) {
var children = [] var children = []