implement m.domFor() and use it internally to move and remove nodes. Fix #2780

This commit is contained in:
Pierre-Yves 2022-06-09 13:50:27 +02:00 committed by Pierre-Yves Gérardy
parent 27fb1ea617
commit 3fd82e6359
8 changed files with 345 additions and 267 deletions

View file

@ -3,6 +3,7 @@
var hyperscript = require("./hyperscript") var hyperscript = require("./hyperscript")
var request = require("./request") var request = require("./request")
var mountRedraw = require("./mount-redraw") var mountRedraw = require("./mount-redraw")
var domFor = require("./render/dom-for")
var m = function m() { return hyperscript.apply(this, arguments) } var m = function m() { return hyperscript.apply(this, arguments) }
m.m = hyperscript m.m = hyperscript
@ -20,5 +21,6 @@ m.parsePathname = require("./pathname/parse")
m.buildPathname = require("./pathname/build") m.buildPathname = require("./pathname/build")
m.vnode = require("./render/vnode") m.vnode = require("./render/vnode")
m.censor = require("./util/censor") m.censor = require("./util/censor")
m.domFor = domFor.domFor
module.exports = m module.exports = m

64
package-lock.json generated
View file

@ -8,9 +8,6 @@
"name": "mithril", "name": "mithril",
"version": "2.2.2", "version": "2.2.2",
"license": "MIT", "license": "MIT",
"dependencies": {
"ospec": "4.0.1"
},
"bin": { "bin": {
"ospec": "ospec/bin/ospec" "ospec": "ospec/bin/ospec"
}, },
@ -30,7 +27,7 @@
"marked": "^4.0.10", "marked": "^4.0.10",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ospec": "^4.0.1", "ospec": "4.1.6",
"pinpoint": "^1.1.0", "pinpoint": "^1.1.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise-native": "^1.0.7", "request-promise-native": "^1.0.7",
@ -347,7 +344,8 @@
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
}, },
"node_modules/bcrypt-pbkdf": { "node_modules/bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
@ -381,6 +379,7 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -564,7 +563,8 @@
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
}, },
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -1429,7 +1429,8 @@
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.1.0", "version": "2.1.0",
@ -1518,6 +1519,7 @@
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@ -1773,6 +1775,7 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@ -1781,7 +1784,8 @@
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
}, },
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
@ -2504,6 +2508,7 @@
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@ -2714,6 +2719,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@ -2751,9 +2757,11 @@
} }
}, },
"node_modules/ospec": { "node_modules/ospec": {
"version": "4.0.1", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/ospec/-/ospec-4.0.1.tgz", "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.1.6.tgz",
"integrity": "sha512-iHx5jkuXh/hU4eEFFf6FH/4vj5RH7041jhuJw7WnqIPHDJgEXkSBdmuD644I/qQm8s5aZEPOpSoVagGCr8ebag==", "integrity": "sha512-Rq+kpRz/ombmIy+g0fAV7mwehtHyQT/J7IjmwVEBw6nbYPxycWgJS3c8BQ0n36kgWOIP5I2SE124cEdunqSH+g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
@ -2815,6 +2823,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -3841,7 +3850,8 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "1.10.2",
@ -4088,7 +4098,8 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
}, },
"bcrypt-pbkdf": { "bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
@ -4119,6 +4130,7 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -4269,7 +4281,8 @@
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -4921,7 +4934,8 @@
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
}, },
"fsevents": { "fsevents": {
"version": "2.1.0", "version": "2.1.0",
@ -4988,6 +5002,7 @@
"version": "7.1.4", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"dev": true,
"requires": { "requires": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@ -5170,6 +5185,7 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": { "requires": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@ -5178,7 +5194,8 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
}, },
"is-arrayish": { "is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
@ -5730,6 +5747,7 @@
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -5900,6 +5918,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -5928,9 +5947,10 @@
} }
}, },
"ospec": { "ospec": {
"version": "4.0.1", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/ospec/-/ospec-4.0.1.tgz", "resolved": "https://registry.npmjs.org/ospec/-/ospec-4.1.6.tgz",
"integrity": "sha512-iHx5jkuXh/hU4eEFFf6FH/4vj5RH7041jhuJw7WnqIPHDJgEXkSBdmuD644I/qQm8s5aZEPOpSoVagGCr8ebag==", "integrity": "sha512-Rq+kpRz/ombmIy+g0fAV7mwehtHyQT/J7IjmwVEBw6nbYPxycWgJS3c8BQ0n36kgWOIP5I2SE124cEdunqSH+g==",
"dev": true,
"requires": { "requires": {
"glob": "^7.1.3" "glob": "^7.1.3"
} }
@ -5975,7 +5995,8 @@
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
}, },
"path-key": { "path-key": {
"version": "2.0.1", "version": "2.0.1",
@ -6753,7 +6774,8 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}, },
"yaml": { "yaml": {
"version": "1.10.2", "version": "1.10.2",

View file

@ -43,7 +43,7 @@
"marked": "^4.0.10", "marked": "^4.0.10",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ospec": "^4.0.1", "ospec": "4.1.6",
"pinpoint": "^1.1.0", "pinpoint": "^1.1.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise-native": "^1.0.7", "request-promise-native": "^1.0.7",

24
render/dom-for.js Normal file
View file

@ -0,0 +1,24 @@
"use strict"
var delayedRemoval = module.exports.delayedRemoval = new WeakMap
module.exports.domFor = function *domFor(vnode, {generation} = {generation: undefined}) {
let {dom, domSize} = vnode
if (dom != null) {
if (domSize == null) {
if (delayedRemoval.get(dom) === generation) {
yield dom
}
} else {
let i = 0, next
while (i < domSize) {
next = dom.nextSibling
if (delayedRemoval.get(dom) === generation) {
yield dom
i++
}
dom = next
}
}
}
}

View file

@ -1,16 +1,20 @@
"use strict" "use strict"
var Vnode = require("../render/vnode") const Vnode = require("../render/vnode")
const {domFor, delayedRemoval} = require("../render/dom-for")
module.exports = function($window) { module.exports = function($window) {
var $doc = $window && $window.document const $doc = $window && $window.document
var currentRedraw
var nameSpace = { const nameSpace = {
svg: "http://www.w3.org/2000/svg", svg: "http://www.w3.org/2000/svg",
math: "http://www.w3.org/1998/Math/MathML" math: "http://www.w3.org/1998/Math/MathML"
} }
let currentRedraw
let currentDOM
let currentRender
function getNameSpace(vnode) { function getNameSpace(vnode) {
return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
} }
@ -87,11 +91,9 @@ module.exports = function($window) {
vnode.dom = temp.firstChild vnode.dom = temp.firstChild
vnode.domSize = temp.childNodes.length vnode.domSize = temp.childNodes.length
// Capture nodes to remove, so we don't confuse them. // Capture nodes to remove, so we don't confuse them.
vnode.instance = []
var fragment = $doc.createDocumentFragment() var fragment = $doc.createDocumentFragment()
var child var child
while (child = temp.firstChild) { while (child = temp.firstChild) {
vnode.instance.push(child)
fragment.appendChild(child) fragment.appendChild(child)
} }
insertNode(parent, fragment, nextSibling) insertNode(parent, fragment, nextSibling)
@ -421,13 +423,12 @@ module.exports = function($window) {
} }
function updateHTML(parent, old, vnode, ns, nextSibling) { function updateHTML(parent, old, vnode, ns, nextSibling) {
if (old.children !== vnode.children) { if (old.children !== vnode.children) {
removeHTML(parent, old) removeDOM(parent, old, undefined)
createHTML(parent, vnode, ns, nextSibling) createHTML(parent, vnode, ns, nextSibling)
} }
else { else {
vnode.dom = old.dom vnode.dom = old.dom
vnode.domSize = old.domSize vnode.domSize = old.domSize
vnode.instance = old.instance
} }
} }
function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { function updateFragment(parent, old, vnode, hooks, nextSibling, ns) {
@ -543,42 +544,18 @@ module.exports = function($window) {
return nextSibling return nextSibling
} }
// This covers a really specific edge case: // This handles fragments with zombie children (removed from vdom, but persisted in DOM throug onbeforeremove)
// - Parent node is keyed and contains child
// - Child is removed, returns unresolved promise in `onbeforeremove`
// - Parent node is moved in keyed diff
// - Remaining children still need moved appropriately
//
// Ideally, I'd track removed nodes as well, but that introduces a lot more
// complexity and I'm not exactly interested in doing that.
function moveNodes(parent, vnode, nextSibling) { function moveNodes(parent, vnode, nextSibling) {
var frag = $doc.createDocumentFragment() if (vnode.dom != null) {
moveChildToFrag(parent, frag, vnode) var target
insertNode(parent, frag, nextSibling) if (vnode.domSize == null) {
} // don't allocate for the common case
function moveChildToFrag(parent, frag, vnode) { target = vnode.dom
// Dodge the recursion overhead in a few of the most common cases.
while (vnode.dom != null && vnode.dom.parentNode === parent) {
if (typeof vnode.tag !== "string") {
vnode = vnode.instance
if (vnode != null) continue
} else if (vnode.tag === "<") {
for (var i = 0; i < vnode.instance.length; i++) {
frag.appendChild(vnode.instance[i])
}
} else if (vnode.tag !== "[") {
// Don't recurse for text nodes *or* elements, just fragments
frag.appendChild(vnode.dom)
} else if (vnode.children.length === 1) {
vnode = vnode.children[0]
if (vnode != null) continue
} else { } else {
for (var i = 0; i < vnode.children.length; i++) { target = $doc.createDocumentFragment()
var child = vnode.children[i] for (const dom of domFor(vnode)) target.appendChild(dom)
if (child != null) moveChildToFrag(parent, frag, child)
}
} }
break insertNode(parent, target, nextSibling)
} }
} }
@ -628,65 +605,42 @@ module.exports = function($window) {
} }
} }
checkState(vnode, original) checkState(vnode, original)
var generation
// If we can, try to fast-path it and avoid all the overhead of awaiting // If we can, try to fast-path it and avoid all the overhead of awaiting
if (!mask) { if (!mask) {
onremove(vnode) onremove(vnode)
removeChild(parent, vnode) removeDOM(parent, vnode, undefined)
} else { } else {
if (stateResult != null) { generation = currentRender
var next = function () { for (const dom of domFor(vnode)) delayedRemoval.set(dom, generation)
function finalizer(a, b) {
return function () {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
if (mask & 1) { mask &= 2; if (!mask) reallyRemove() } if (mask & a) { mask &= b; if (!mask) {
checkState(vnode, original)
onremove(vnode)
removeDOM(parent, vnode, generation)
} }
} }
stateResult.then(next, next) }
if (stateResult != null) {
stateResult.finally(finalizer(1, 2))
} }
if (attrsResult != null) { if (attrsResult != null) {
var next = function () { attrsResult.finally(finalizer(2, 1))
// eslint-disable-next-line no-bitwise
if (mask & 2) { mask &= 1; if (!mask) reallyRemove() }
}
attrsResult.then(next, next)
} }
} }
}
function removeDOM(parent, vnode, generation) {
if (vnode.dom == null) return
if (vnode.domSize == null) {
// don't allocate for the common case
if (delayedRemoval.get(vnode.dom) === generation) parent.removeChild(vnode.dom)
} else {
for (const dom of domFor(vnode, {generation})) parent.removeChild(dom)
}
}
function reallyRemove() {
checkState(vnode, original)
onremove(vnode)
removeChild(parent, vnode)
}
}
function removeHTML(parent, vnode) {
for (var i = 0; i < vnode.instance.length; i++) {
parent.removeChild(vnode.instance[i])
}
}
function removeChild(parent, vnode) {
// Dodge the recursion overhead in a few of the most common cases.
while (vnode.dom != null && vnode.dom.parentNode === parent) {
if (typeof vnode.tag !== "string") {
vnode = vnode.instance
if (vnode != null) continue
} else if (vnode.tag === "<") {
removeHTML(parent, vnode)
} else {
if (vnode.tag !== "[") {
parent.removeChild(vnode.dom)
if (!Array.isArray(vnode.children)) break
}
if (vnode.children.length === 1) {
vnode = vnode.children[0]
if (vnode != null) continue
} else {
for (var i = 0; i < vnode.children.length; i++) {
var child = vnode.children[i]
if (child != null) removeChild(parent, child)
}
}
}
break
}
}
function onremove(vnode) { function onremove(vnode) {
if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode)
@ -946,8 +900,6 @@ module.exports = function($window) {
return true return true
} }
var currentDOM
return function(dom, vnodes, redraw) { return function(dom, vnodes, redraw) {
if (!dom) throw new TypeError("DOM element being rendered to does not exist.") if (!dom) throw new TypeError("DOM element being rendered to does not exist.")
if (currentDOM != null && dom.contains(currentDOM)) { if (currentDOM != null && dom.contains(currentDOM)) {
@ -961,6 +913,7 @@ module.exports = function($window) {
currentDOM = dom currentDOM = dom
currentRedraw = typeof redraw === "function" ? redraw : undefined currentRedraw = typeof redraw === "function" ? redraw : undefined
currentRender = {}
try { try {
// First time rendering into a node clears it out // First time rendering into a node clears it out
if (dom.vnodes == null) dom.textContent = "" if (dom.vnodes == null) dom.textContent = ""

View file

@ -0,0 +1,176 @@
var o = require("ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
var m = require("../../render/hyperscript")
var fragment = require("../../render/fragment")
var domFor = require('../../render/dom-for').domFor
o.spec("domFor(vnode)", function() {
var $window, root, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.createElement("div")
render = vdom($window)
})
o('works for simple vnodes', function() {
render(root, m('div', {oncreate(vnode){
let n = 0
for (const dom of domFor(vnode)) {
o(dom).equals(root.firstChild)
o(++n).equals(1)
}
}}))
})
o('works for fragments', function () {
render(root, fragment({
oncreate(vnode){
let n = 0
for (const dom of domFor(vnode)) {
o(dom).equals(root.childNodes[n])
n++
}
o(n).equals(2)
}
}, [
m('a'),
m('b')
]))
})
o('works in fragments with children that have delayed removal', function() {
function oncreate(vnode){
o(root.childNodes.length).equals(3)
o(root.childNodes[0].nodeName).equals('A')
o(root.childNodes[1].nodeName).equals('B')
o(root.childNodes[2].nodeName).equals('C')
const iter = domFor(vnode)
o(iter.next()).deepEquals({done:false, value: root.childNodes[0]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[1]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[2]})
o(iter.next().done).deepEquals(true)
o(root.childNodes.length).equals(3)
}
function onupdate(vnode) {
// the b node is still present in the DOM
o(root.childNodes.length).equals(3)
o(root.childNodes[0].nodeName).equals('A')
o(root.childNodes[1].nodeName).equals('B')
o(root.childNodes[2].nodeName).equals('C')
const iter = domFor(vnode)
o(iter.next()).deepEquals({done:false, value: root.childNodes[0]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[2]})
o(iter.next().done).deepEquals(true)
o(root.childNodes.length).equals(3)
}
render(root, fragment(
{oncreate, onupdate},
[
m('a'),
m('b', {onbeforeremove(){return {then(){}, finally(){}}}}),
m('c')
]
))
render(root, fragment(
{oncreate, onupdate},
[
m('a'),
null,
m('c'),
]
))
})
components.forEach(function(cmp){
o.spec(cmp.kind, function(){
var createComponent = cmp.create
o('works for components that return one element', function() {
const C = createComponent({
view(){return m('div')},
oncreate(vnode){
let n = 0
for (const dom of domFor(vnode)) {
o(dom).equals(root.firstChild)
o(++n).equals(1)
}
}
})
render(root, m(C))
})
o('works for components that return fragments', function () {
const oncreate = o.spy(function oncreate(vnode){
o(root.childNodes.length).equals(3)
o(root.childNodes[0].nodeName).equals('A')
o(root.childNodes[1].nodeName).equals('B')
o(root.childNodes[2].nodeName).equals('C')
const iter = domFor(vnode)
o(iter.next()).deepEquals({done:false, value: root.childNodes[0]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[1]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[2]})
o(iter.next().done).deepEquals(true)
o(root.childNodes.length).equals(3)
})
const C = createComponent({
view({children}){return children},
oncreate
})
render(root, m(C, [
m('a'),
m('b'),
m('c')
]))
o(oncreate.callCount).equals(1)
})
o('works for components that return fragments with delayed removal', function () {
const onbeforeremove = o.spy(function onbeforeremove(){return {then(){}, finally(){}}})
const oncreate = o.spy(function oncreate(vnode){
o(root.childNodes.length).equals(3)
o(root.childNodes[0].nodeName).equals('A')
o(root.childNodes[1].nodeName).equals('B')
o(root.childNodes[2].nodeName).equals('C')
const iter = domFor(vnode)
o(iter.next()).deepEquals({done:false, value: root.childNodes[0]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[1]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[2]})
o(iter.next().done).deepEquals(true)
o(root.childNodes.length).equals(3)
})
const onupdate = o.spy(function onupdate(vnode) {
o(root.childNodes.length).equals(3)
o(root.childNodes[0].nodeName).equals('A')
o(root.childNodes[1].nodeName).equals('B')
o(root.childNodes[2].nodeName).equals('C')
const iter = domFor(vnode)
o(iter.next()).deepEquals({done:false, value: root.childNodes[0]})
o(iter.next()).deepEquals({done:false, value: root.childNodes[2]})
o(iter.next().done).deepEquals(true)
o(root.childNodes.length).equals(3)
})
const C = createComponent({
view({children}){return children},
oncreate,
onupdate
})
render(root, m(C, [
m('a'),
m('b', {onbeforeremove}),
m('c')
]))
render(root, m(C, [
m('a'),
null,
m('c')
]))
o(oncreate.callCount).equals(1)
o(onupdate.callCount).equals(1)
o(onbeforeremove.callCount).equals(1)
})
})
})
})

View file

@ -195,13 +195,13 @@ o.spec("onremove", function() {
}) })
// Warning: this test is complicated because it's replicating a race condition. // Warning: this test is complicated because it's replicating a race condition.
o("removes correct nodes when child delays removal, parent removes, then child resolves", function () { o("removes correct nodes when child delays removal, parent removes, then child resolves", function () {
// Sugar over the complexity - I need to test the entire tree for consistency. // Custom assertion - we need to test the entire tree for consistency.
function expect(expectedPairs) { function template(tpl) {return function (root) {
var expected = [] var expected = []
for (var i = 0; i < expectedPairs.length; i++) { for (var i = 0; i < tpl.length; i++) {
var name = expectedPairs[i][0] var name = tpl[i][0]
var text = expectedPairs[i][1] var text = tpl[i][1]
expected.push({ expected.push({
name: name, name: name,
firstType: name === "#text" ? null : "#text", firstType: name === "#text" ? null : "#text",
@ -222,23 +222,30 @@ o.spec("onremove", function() {
text: textNode.nodeValue, text: textNode.nodeValue,
}) })
} }
actual = JSON.stringify(actual, null, ' ')
expected = JSON.stringify(expected, null, ' ')
return {
pass: actual === expected,
message:
`${expected}
expected, got
${actual}`
}
}}
o(actual).deepEquals(expected) var finallyCB
}
var resolve
function update(id, showParent, showChild) { function update(id, showParent, showChild) {
render(root, render(root,
m("div", m("div",
showParent && fragment( showParent && fragment(
"", // Required "", // Required
showChild && fragment({ showChild && fragment(
onbeforeremove: function () { {
return {then: function (r) { resolve = r }} onbeforeremove: function () {
return {then(){}, finally: function (fcb) { finallyCB = fcb }}
},
}, },
}, m("div", id)
m("div", id)
) )
) )
) )
@ -246,170 +253,61 @@ o.spec("onremove", function() {
} }
update("1", true, true) update("1", true, true)
expect([ o(root).satisfies(template([
["#text", ""], ["#text", ""],
["DIV", "1"], ["DIV", "1"],
]) ]))
o(resolve).equals(undefined) o(finallyCB).equals(undefined)
update("2", true, false) update("2", true, false)
expect([
o(root).satisfies(template([
["#text", ""], ["#text", ""],
["DIV", "1"], ["DIV", "1"],
]) ]))
o(typeof resolve).equals("function") o(typeof finallyCB).equals("function")
var original = resolve
var original = finallyCB
update("3", true, true) update("3", true, true)
expect([
o(root).satisfies(template([
["#text", ""], ["#text", ""],
["DIV", "1"], ["DIV", "1"],
["DIV", "3"], ["DIV", "3"],
]) ]))
o(resolve).equals(original) o(finallyCB).equals(original)
update("4", false, true) update("4", false, true)
expect([
o(root).satisfies(template([
["DIV", "1"], ["DIV", "1"],
]) ]))
o(resolve).equals(original) o(finallyCB).equals(original)
update("5", true, true) update("5", true, true)
expect([
o(root).satisfies(template([
["DIV", "1"], ["DIV", "1"],
["#text", ""], ["#text", ""],
["DIV", "5"], ["DIV", "5"],
]) ]))
o(resolve).equals(original) o(finallyCB).equals(original)
resolve() finallyCB()
expect([
o(root).satisfies(template([
["#text", ""], ["#text", ""],
["DIV", "5"], ["DIV", "5"],
]) ]))
o(resolve).equals(original) o(finallyCB).equals(original)
update("6", true, true) update("6", true, true)
expect([ o(root).satisfies(template([
["#text", ""], ["#text", ""],
["DIV", "6"], ["DIV", "6"],
]) ]))
o(resolve).equals(original) o(finallyCB).equals(original)
})
// Warning: this test is complicated because it's replicating a race condition.
o("removes correct nodes when child delays removal, parent removes, then child resolves + rejects both", function () {
// Sugar over the complexity - I need to test the entire tree for consistency.
function expect(expectedPairs) {
var expected = []
for (var i = 0; i < expectedPairs.length; i++) {
var name = expectedPairs[i][0]
var text = expectedPairs[i][1]
expected.push({
name: name,
firstType: name === "#text" ? null : "#text",
text: text,
})
}
var actual = []
var list = root.firstChild.childNodes
for (var i = 0; i < list.length; i++) {
var current = list[i]
var textNode = current.childNodes.length === 1
? current.firstChild
: current
actual.push({
name: current.nodeName,
firstType: textNode === current ? null : textNode.nodeName,
text: textNode.nodeValue,
})
}
o(actual).deepEquals(expected)
}
var resolve, reject
function update(id, showParent, showChild) {
render(root,
m("div",
showParent && fragment(
"", // Required
showChild && fragment({
onbeforeremove: function () {
return {then: function (res, rej) {
resolve = res
reject = rej
}}
},
},
m("div", id)
)
)
)
)
}
update("1", true, true)
expect([
["#text", ""],
["DIV", "1"],
])
o(resolve).equals(undefined)
update("2", true, false)
expect([
["#text", ""],
["DIV", "1"],
])
o(typeof resolve).equals("function")
var originalResolve = resolve
var originalReject = reject
update("3", true, true)
expect([
["#text", ""],
["DIV", "1"],
["DIV", "3"],
])
o(resolve).equals(originalResolve)
o(reject).equals(originalReject)
update("4", false, true)
expect([
["DIV", "1"],
])
o(resolve).equals(originalResolve)
o(reject).equals(originalReject)
update("5", true, true)
expect([
["DIV", "1"],
["#text", ""],
["DIV", "5"],
])
o(resolve).equals(originalResolve)
o(reject).equals(originalReject)
resolve()
reject()
reject()
resolve()
expect([
["#text", ""],
["DIV", "5"],
])
o(resolve).equals(originalResolve)
o(reject).equals(originalReject)
update("6", true, true)
expect([
["#text", ""],
["DIV", "6"],
])
o(resolve).equals(originalResolve)
o(reject).equals(originalReject)
}) })
}) })
}) })

View file

@ -99,12 +99,15 @@ module.exports = function(options) {
} }
} }
function removeChild(child) { function removeChild(child) {
if (child == null || typeof child !== 'object' || !("nodeType" in child)) {
throw new TypeError("Failed to execute removeChild, parameter is not of type 'Node'")
}
var index = this.childNodes.indexOf(child) var index = this.childNodes.indexOf(child)
if (index > -1) { if (index > -1) {
this.childNodes.splice(index, 1) this.childNodes.splice(index, 1)
child.parentNode = null child.parentNode = null
} }
else throw new TypeError("Failed to execute 'removeChild'") else throw new TypeError("Failed to execute 'removeChild', child not found in parent")
} }
function insertBefore(child, reference) { function insertBefore(child, reference) {
var ancestor = this var ancestor = this