diff --git a/Gruntfile.js b/Gruntfile.js index 1beee29f..3497a68d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,6 +1,6 @@ module.exports = function(grunt) { - var version = "0.1.16" + var version = "0.1.17" var inputFolder = "./docs" var tempFolder = "./temp" diff --git a/docs/mithril.md b/docs/mithril.md index ae50bc34..2be4d04f 100644 --- a/docs/mithril.md +++ b/docs/mithril.md @@ -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). 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}) ``` + 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) 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) diff --git a/mithril.js b/mithril.js index 11c92a6f..986320d3 100644 --- a/mithril.js +++ b/mithril.js @@ -41,9 +41,10 @@ Mithril = m = new function app(window) { if (cached !== null && cached !== undefined) { if (parentCache && parentCache.nodes) { 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.nodes = [] @@ -74,7 +75,7 @@ Mithril = m = new function app(window) { for (var i = 0, change; change = changes[i]; i++) { if (change.action == DELETION) { - clear(cached[change.index].nodes) + clear(cached[change.index].nodes, cached[change.index]) newCached.splice(change.index, 1) } if (change.action == INSERTION) { @@ -108,7 +109,10 @@ Mithril = m = new function app(window) { if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes) } 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++) { if (node.parentNode === null) parentElement.appendChild(node) @@ -119,7 +123,7 @@ Mithril = m = new function app(window) { } 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 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 (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 { @@ -163,7 +167,7 @@ Mithril = m = new function app(window) { nodes = cached.nodes if (!editable || editable !== window.document.activeElement) { if (data.$trusted) { - clear(nodes) + clear(nodes, cached) nodes = injectHTML(parentElement, index, data) } else { @@ -171,7 +175,7 @@ Mithril = m = new function app(window) { else if (editable) editable.innerHTML = data else { if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string - clear(cached.nodes) + clear(cached.nodes, cached) nodes = [window.document.createTextNode(data)] } parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) @@ -221,10 +225,20 @@ Mithril = m = new function app(window) { } return cachedAttrs } - function clear(nodes) { - for (var i = nodes.length - 1; i > -1; i--) if (nodes[i] && nodes[i].parentNode) nodes[i].parentNode.removeChild(nodes[i]) + 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]) + cached = [].concat(cached) + if (cached[i]) unload(cached[i]) + } + } 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) { var nextSibling = parentElement.childNodes[index] if (nextSibling) { diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index 73ac1326..d720a671 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -864,6 +864,297 @@ function testMithril(mock) { mock.performance.$elapse(50) //teardown 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 //m.prop