add context unload event listener

This commit is contained in:
Leo Horie 2014-06-25 22:45:21 -04:00
parent 88bcb8a495
commit 42f21181f8
4 changed files with 360 additions and 11 deletions

View file

@ -1,6 +1,6 @@
module.exports = function(grunt) { module.exports = function(grunt) {
var version = "0.1.16" var version = "0.1.17"
var inputFolder = "./docs" var inputFolder = "./docs"
var tempFolder = "./temp" var tempFolder = "./temp"

View file

@ -217,6 +217,29 @@ m("div", {config: alertsRedrawCount})
--- ---
If the `context` object that is passed to a `config` function has a property called `onunload`, this function will be called when the element gets detached from the document by Mithril's diff engine.
This is useful if there are cleanup tasks that need to be run when an element is destroyed (e.g. clearing `setTimeout`'s, etc)
```javascript
function unloadable(element, isInit, context) {
context.timer = setTimeout(function() {
alert("timed out!");
}, 1000);
context.onunload = function() {
clearTimeout(context.timer);
console.log("unloaded the div");
}
};
m.render(document, m("div", {config: unloadable}));
m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets called
```
---
You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively). You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively).
Mithril automatically figures out the correct XML namespaces when it sees an SVG island in the virtual DOM tree. Mithril automatically figures out the correct XML namespaces when it sees an SVG island in the virtual DOM tree.
@ -369,6 +392,27 @@ where:
m("div", {config: alertsRedrawCount}) m("div", {config: alertsRedrawCount})
``` ```
If the `context` object that is passed to a `config` function has a property called `onunload`, this function will be called when the element gets detached from the document by Mithril's diff engine.
This is useful if there are cleanup tasks that need to be run when an element is destroyed (e.g. clearing `setTimeout`'s, etc)
```javascript
function unloadable(element, isInit, context) {
context.timer = setTimeout(function() {
alert("timed out!");
}, 1000);
context.onunload = function() {
clearTimeout(context.timer);
console.log("unloaded the div");
}
};
m.render(document, m("div", {config: unloadable}));
m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets called
```
- **Children children** (optional) - **Children children** (optional)
If this argument is a string, it will be rendered as a text node. To render a string as HTML, see [`m.trust`](mithril.trust) If this argument is a string, it will be rendered as a text node. To render a string as HTML, see [`m.trust`](mithril.trust)

View file

@ -41,9 +41,10 @@ Mithril = m = new function app(window) {
if (cached !== null && cached !== undefined) { if (cached !== null && cached !== undefined) {
if (parentCache && parentCache.nodes) { if (parentCache && parentCache.nodes) {
var offset = index - parentIndex var offset = index - parentIndex
clear(parentCache.nodes.slice(offset, offset + (dataType == "[object Array]" ? data : cached.nodes).length)) var end = offset + (dataType == "[object Array]" ? data : cached.nodes).length
clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end))
} }
else clear(cached.nodes) else clear(cached.nodes, cached)
} }
cached = new data.constructor cached = new data.constructor
cached.nodes = [] cached.nodes = []
@ -74,7 +75,7 @@ Mithril = m = new function app(window) {
for (var i = 0, change; change = changes[i]; i++) { for (var i = 0, change; change = changes[i]; i++) {
if (change.action == DELETION) { if (change.action == DELETION) {
clear(cached[change.index].nodes) clear(cached[change.index].nodes, cached[change.index])
newCached.splice(change.index, 1) newCached.splice(change.index, 1)
} }
if (change.action == INSERTION) { if (change.action == INSERTION) {
@ -108,7 +109,10 @@ Mithril = m = new function app(window) {
if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes) if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes)
} }
for (var i = nodes.length, node; node = cached.nodes[i]; i++) { for (var i = nodes.length, node; node = cached.nodes[i]; i++) {
if (node.parentNode !== null && node.parentNode.childNodes.length != nodes.length) node.parentNode.removeChild(node) if (node.parentNode !== null && node.parentNode.childNodes.length != nodes.length) {
node.parentNode.removeChild(node)
if (cached[i]) unload(cached[i])
}
} }
for (var i = cached.nodes.length, node; node = nodes[i]; i++) { for (var i = cached.nodes.length, node; node = nodes[i]; i++) {
if (node.parentNode === null) parentElement.appendChild(node) if (node.parentNode === null) parentElement.appendChild(node)
@ -119,7 +123,7 @@ Mithril = m = new function app(window) {
} }
else if (dataType == "[object Object]") { else if (dataType == "[object Object]") {
if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) clear(cached.nodes) if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) clear(cached.nodes, cached)
if (typeof data.tag != "string") return if (typeof data.tag != "string") return
var node, isNew = cached.nodes.length === 0 var node, isNew = cached.nodes.length === 0
@ -143,7 +147,7 @@ Mithril = m = new function app(window) {
if (shouldReattach === true) parentElement.insertBefore(node, parentElement.childNodes[index] || null) if (shouldReattach === true) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
} }
if (type.call(data.attrs["config"]) == "[object Function]") { if (type.call(data.attrs["config"]) == "[object Function]") {
configs.push(data.attrs["config"].bind(window, node, !isNew, cached.configContext = cached.configContext || {})) configs.push(data.attrs["config"].bind(window, node, !isNew, cached.configContext = cached.configContext || {}, cached))
} }
} }
else { else {
@ -163,7 +167,7 @@ Mithril = m = new function app(window) {
nodes = cached.nodes nodes = cached.nodes
if (!editable || editable !== window.document.activeElement) { if (!editable || editable !== window.document.activeElement) {
if (data.$trusted) { if (data.$trusted) {
clear(nodes) clear(nodes, cached)
nodes = injectHTML(parentElement, index, data) nodes = injectHTML(parentElement, index, data)
} }
else { else {
@ -171,7 +175,7 @@ Mithril = m = new function app(window) {
else if (editable) editable.innerHTML = data else if (editable) editable.innerHTML = data
else { else {
if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string
clear(cached.nodes) clear(cached.nodes, cached)
nodes = [window.document.createTextNode(data)] nodes = [window.document.createTextNode(data)]
} }
parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null)
@ -221,10 +225,20 @@ Mithril = m = new function app(window) {
} }
return cachedAttrs return cachedAttrs
} }
function clear(nodes) { function clear(nodes, cached) {
for (var i = nodes.length - 1; i > -1; i--) if (nodes[i] && nodes[i].parentNode) nodes[i].parentNode.removeChild(nodes[i]) for (var i = nodes.length - 1; i > -1; i--) {
if (nodes[i] && nodes[i].parentNode) {
nodes[i].parentNode.removeChild(nodes[i])
cached = [].concat(cached)
if (cached[i]) unload(cached[i])
}
}
nodes.length = 0 nodes.length = 0
} }
function unload(cached) {
if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload()
if (cached.children instanceof Array) for (var i = 0; i < cached.children.length; i++) unload(cached.children[i])
}
function injectHTML(parentElement, index, data) { function injectHTML(parentElement, index, data) {
var nextSibling = parentElement.childNodes[index] var nextSibling = parentElement.childNodes[index]
if (nextSibling) { if (nextSibling) {

View file

@ -864,6 +864,297 @@ function testMithril(mock) {
mock.performance.$elapse(50) //teardown mock.performance.$elapse(50) //teardown
return route1 == "/" && route2 == "/test13" return route1 == "/" && route2 == "/test13"
}) })
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
}
},
"/test14": {controller: function() {}, view: function() {}}
})
mock.performance.$elapse(50)
m.route("/test14")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return [
m("div"),
m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
},
"/test15": {
controller: function() {},
view: function() {
return [m("div")]
}
}
})
mock.performance.$elapse(50)
m.route("/test15")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
}
},
"/test16": {
controller: function() {},
view: function() {
return m("a")
}
}
})
mock.performance.$elapse(50)
m.route("/test16")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return [
m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
},
"/test17": {
controller: function() {},
view: function() {
return m("a")
}
}
})
mock.performance.$elapse(50)
m.route("/test17")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
}
},
"/test18": {
controller: function() {},
view: function() {
return [m("a")]
}
}
})
mock.performance.$elapse(50)
m.route("/test18")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return [
m("div", {
key: 1,
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
},
"/test19": {
controller: function() {},
view: function() {
return [
m("div", {
key: 1,
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
}
})
mock.performance.$elapse(50)
m.route("/test19")
mock.performance.$elapse(50) //teardown
return unloaded == 0
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return [
m("div", {
key: 1,
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
},
"/test20": {
controller: function() {},
view: function() {
return [
m("div", {
key: 2,
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
}
})
mock.performance.$elapse(50)
m.route("/test20")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
test(function() {
mock.performance.$elapse(50) //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var unloaded = 0
m.route.mode = "search"
m.route(root, "/", {
"/": {
controller: function() {},
view: function() {
return [
m("div", {
key: 1,
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
},
"/test21": {
controller: function() {},
view: function() {
return [
m("div", {
config: function(el, init, ctx) {
ctx.onunload = function() {
unloaded++
}
}
})
]
}
}
})
mock.performance.$elapse(50)
m.route("/test21")
mock.performance.$elapse(50) //teardown
return unloaded == 1
})
//end m.route //end m.route
//m.prop //m.prop