From 84f472cb9ebc65744ba26110310b3c25f8e8aefc Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Thu, 12 Feb 2015 21:02:27 -0500 Subject: [PATCH 1/2] context reuse flag --- mithril.js | 10 +- tests/mithril-tests.js | 436 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 429 insertions(+), 17 deletions(-) diff --git a/mithril.js b/mithril.js index 3dcfb4b4..9c49da7b 100644 --- a/mithril.js +++ b/mithril.js @@ -238,7 +238,7 @@ var m = (function app(window, undefined) { var dataAttrKeys = Object.keys(data.attrs) var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) //if an element is different enough from the one in cache, recreate it - if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) { + if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.reuse !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.reuse === false)) { if (cached.nodes.length) clear(cached.nodes); if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() } @@ -391,7 +391,10 @@ var m = (function app(window, undefined) { if (nodes.length != 0) nodes.length = 0 } function unload(cached) { - if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload(); + if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) { + cached.configContext.onunload(); + cached.configContext.onunload = null + } if (cached.children) { if (type.call(cached.children) === ARRAY) { for (var i = 0, child; child = cached.children[i]; i++) unload(child) @@ -540,10 +543,9 @@ var m = (function app(window, undefined) { m.redraw.strategy = m.prop(); var blank = function() {return ""} function redraw() { - var forceRedraw = m.redraw.strategy() === "all"; for (var i = 0, root; root = roots[i]; i++) { if (controllers[i]) { - m.render(root, modules[i].view ? modules[i].view(controllers[i]) : blank(), forceRedraw) + m.render(root, modules[i].view ? modules[i].view(controllers[i]) : blank()) } } //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index a6c161ed..00b30e34 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -86,6 +86,19 @@ function testMithril(mock) { return unloaded }) + test(function() { + var root = mock.document.createElement("div") + var module = {}, unloaded = false + module.controller = function() { + this.onunload = function() {unloaded = true} + } + module.view = function() {} + m.module(root, module) + m.module(root, {controller: function() {}, view: function() {}}) + + return unloaded === true + }) + m.redraw.strategy(undefined) //teardown for m.module tests //m.withAttr test(function() { @@ -400,17 +413,6 @@ function testMithril(mock) { var valueAfter = root.childNodes[0].style.background return valueBefore === "red" && valueAfter === undefined }) - test(function() { - var root = mock.document.createElement("div") - var module = {}, unloaded = false - module.controller = function() { - this.onunload = function() {unloaded = true} - } - module.view = function() {} - m.module(root, module) - m.module(root, {controller: function() {}, view: function() {}}) - return unloaded === true - }) test(function() { //https://github.com/lhorie/mithril.js/issues/87 var root = mock.document.createElement("div") @@ -490,7 +492,7 @@ function testMithril(mock) { }) test(function() { var root = mock.document.createElement("div") - + var success = false m.render(root, m("div", {config: function(elem, isInitialized, ctx) {ctx.data = 1}})) m.render(root, m("div", {config: function(elem, isInitialized, ctx) {success = ctx.data === 1}})) @@ -1374,7 +1376,7 @@ function testMithril(mock) { test(function() { mock.requestAnimationFrame.$resolve() //setup mock.location.search = "?" - + var root = mock.document.createElement("div") var unloaded = 0 m.route.mode = "search" @@ -1746,6 +1748,414 @@ function testMithril(mock) { return mock.history.$$length == 0 }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }}) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = a.view + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + ctx.reuse = true + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + ctx.reuse = false + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var initCount = 0 + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("div", m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }})) + } + + var b = {} + b.controller = function() {m.redraw.strategy("diff")} + b.view = function() { + return m("section", m("a", {config: function(el, init, ctx) { + if (!init) initCount++ + }})) + } + + m.route(root, "/a", { + "/a": a, + "/b": b, + }) + mock.requestAnimationFrame.$resolve() + + m.route("/b") + + mock.requestAnimationFrame.$resolve() + + return initCount == 1 + }) //end m.route //m.prop From 71a19a27a948478a2f9eb9ce9ad5d6dbe0508f4f Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Mon, 2 Mar 2015 22:25:52 -0500 Subject: [PATCH 2/2] document flag --- docs/mithril.md | 62 +++++++++++++++++++++++++++++++++++++++ mithril.js | 8 ++--- tests/mithril-tests.js | 66 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/docs/mithril.md b/docs/mithril.md index 12717a45..a4483309 100644 --- a/docs/mithril.md +++ b/docs/mithril.md @@ -8,6 +8,7 @@ - [Accessing the real DOM element](#accessing-the-real-dom-element) - [Persisting config data](#persisting-config-data) - [Destructors](#destructors) +- [Persising DOM elements across route changes](#persising-dom-elements-across-route-changes) - [SVG](#svg) - [Dealing with focus](#dealing-with-focus) - [Dealing with sorting and deleting in lists](#dealing-with-sorting-and-deleting-in-lists) @@ -342,6 +343,67 @@ m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets cal --- +#### Persising DOM elements across route changes + +When using the [router](mithril.route.md), a route change recreates the DOM tree from scratch in order to unload plugins from the previous page. If you want to keep a DOM element intact across a route change, you can set the `retain` flag in the config's context object. + +In the example below, there are two routes, each of which loads a module when a user navigates to their respective URLs. Both modules use a `menu` template, which contains links for navigation between the two modules, and an expensive-to-reinitialize element. Setting `context.retain = true` in the element's config function allows the span to stay intact after a route change. + +```javascript +//a menu template +var menu = function() { + return m("div", [ + m("a[href='/']", {config: m.route}, "Home"), + m("a[href='/contact']", {config: m.route}, "Contact"), + //an expensive-to-initialize DOM element + m("span", {config: persistent}) + ]) +} +//a configuration that persists across route changes +function persistent(el, isInit, context) { + context.retain = true + + if (!isInit) { + //only runs once, even if you move back and forth between `/` and `/contact` + doSomethingExpensive(el) + } +} + +//modules that use the menu above +var Home = { + controller: function() {}, + view: function() { + return [ + menu(), + m("h1", "Home") + ] + } +} +var Contact = { + view: function() { + return [ + menu(), + m("h2", "Contact") + ] + } +} + +m.route(document.body, "/", { + "/": Home, + "/contact": Contact +}) +``` + +Note that even if you set `context.retain = true`, the element will still be destroyed and recreated if it is different enough from the existing element. An element is considered "different enough" if: + +- the tag name changes, or +- the list of HTML attributes changes, or +- the value of the element's id attribute changes + +In addition, setting `context.retain = false` will also cause the element to be recreated, even if it is not considered different enough. + +--- + #### SVG You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively). diff --git a/mithril.js b/mithril.js index 2c2cf7ce..2997b392 100644 --- a/mithril.js +++ b/mithril.js @@ -94,7 +94,7 @@ var m = (function app(window, undefined) { //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} //- it simplifies diffing code //data.toString() is null if data is the return value of Console.log in Firefox - if (data == null || data.toString() == null) data = ""; + try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""} if (data.subtree === "retain") return cached; var cachedType = type.call(cached), dataType = type.call(data); if (cached == null || cachedType !== dataType) { @@ -120,7 +120,7 @@ var m = (function app(window, undefined) { len = data.length } } - + var nodes = [], intact = cached.length === data.length, subArrayCount = 0; //keys algorithm: sort elements without recreating them if keys are present @@ -238,7 +238,7 @@ var m = (function app(window, undefined) { var dataAttrKeys = Object.keys(data.attrs) var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) //if an element is different enough from the one in cache, recreate it - if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.reuse !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.reuse === false)) { + if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.retain !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { if (cached.nodes.length) clear(cached.nodes); if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() } @@ -274,7 +274,7 @@ var m = (function app(window, undefined) { } //schedule configs to be called. They are called after `build` finishes running if (typeof data.attrs["config"] === FUNCTION) { - var context = cached.configContext = cached.configContext || {}; + var context = cached.configContext = cached.configContext || {retain: m.redraw.strategy() == "diff"}; // bind var callback = function(data, args) { diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index b451f0af..7c57748d 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -1783,6 +1783,48 @@ function testMithril(mock) { mock.requestAnimationFrame.$resolve() mock.location.search = "?" + var root = mock.document.createElement("div") + var value + + var a = {} + a.controller = function() {} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + value = ctx.retain + }}) + } + + m.route(root, "/a", { + "/a": a + }) + + return value === false + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + + var root = mock.document.createElement("div") + var value + + var a = {} + a.controller = function() {m.redraw.strategy("diff")} + a.view = function() { + return m("a", {config: function(el, init, ctx) { + value = ctx.retain + }}) + } + + m.route(root, "/a", { + "/a": a + }) + + return value === true + }) + test(function() { + mock.requestAnimationFrame.$resolve() + mock.location.search = "?" + var root = mock.document.createElement("div") var initCount = 0 @@ -1790,7 +1832,7 @@ function testMithril(mock) { a.controller = function() {} a.view = function() { return m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }}) } @@ -1822,7 +1864,7 @@ function testMithril(mock) { a.controller = function() {} a.view = function() { return m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }}) } @@ -1885,7 +1927,7 @@ function testMithril(mock) { a.controller = function() {m.redraw.strategy("diff")} a.view = function() { return m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }}) } @@ -1917,7 +1959,7 @@ function testMithril(mock) { a.controller = function() {m.redraw.strategy("diff")} a.view = function() { return m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }}) } @@ -1949,7 +1991,7 @@ function testMithril(mock) { a.controller = function() {} a.view = function() { return m("div", m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }})) } @@ -1958,7 +2000,7 @@ function testMithril(mock) { b.controller = function() {} b.view = function() { return m("section", m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }})) } @@ -1986,7 +2028,7 @@ function testMithril(mock) { a.controller = function() {} a.view = function() { return m("div", m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }})) } @@ -1995,7 +2037,7 @@ function testMithril(mock) { b.controller = function() {} b.view = function() { return m("section", m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }})) } @@ -2058,7 +2100,7 @@ function testMithril(mock) { a.controller = function() {m.redraw.strategy("diff")} a.view = function() { return m("div", m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }})) } @@ -2067,7 +2109,7 @@ function testMithril(mock) { b.controller = function() {m.redraw.strategy("diff")} b.view = function() { return m("section", m("a", {config: function(el, init, ctx) { - ctx.reuse = true + ctx.retain = true if (!init) initCount++ }})) } @@ -2095,7 +2137,7 @@ function testMithril(mock) { a.controller = function() {m.redraw.strategy("diff")} a.view = function() { return m("div", m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }})) } @@ -2104,7 +2146,7 @@ function testMithril(mock) { b.controller = function() {m.redraw.strategy("diff")} b.view = function() { return m("section", m("a", {config: function(el, init, ctx) { - ctx.reuse = false + ctx.retain = false if (!init) initCount++ }})) }