From 4281773df977038e5bd8df1cb12595eb4436f9ce Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Mon, 11 Aug 2014 23:15:53 -0400 Subject: [PATCH] redraw strategy --- docs/comparison.md | 2 +- docs/getting-started.md | 2 +- docs/layout/index.html | 2 +- docs/mithril.redraw.md | 59 ++++++++++++++++++++++++- docs/mithril.render.md | 5 ++- mithril.js | 43 ++++++++++--------- tests/mithril-tests.js | 95 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 182 insertions(+), 26 deletions(-) diff --git a/docs/comparison.md b/docs/comparison.md index 09b020bc..190e7b53 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -6,7 +6,7 @@ This page aims to provide a comparison between Mithril and some of the most wide ### Code Size -One of the most obvious differences between Mithril and most frameworks is in file size: Mithril is around 4kb gzipped and has no dependencies on other libraries. +One of the most obvious differences between Mithril and most frameworks is in file size: Mithril is around 5kb gzipped and has no dependencies on other libraries. Note that while a small gzipped size can look appealing, that number is often used to "hide the weight" of the uncompressed code: remember that the decompressed Javascript still needs to be parsed and evaluated on every page load, and this cost (which can be in the dozens of milliseconds range for some frameworks in some browsers) cannot be cached. diff --git a/docs/getting-started.md b/docs/getting-started.md index 5fb5cc9e..eb255683 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,7 +4,7 @@ Mithril is a client-side Javascript MVC framework, i.e. it's a tool to make application code divided into a data layer (called **M**odel), a UI layer (called **V**iew), and a glue layer (called **C**ontroller) -Mithril is around 4kb gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization. +Mithril is around 5kb gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization. The goal of the framework is to make application code discoverable, readable and maintainable, and hopefully help you become an even better developer. diff --git a/docs/layout/index.html b/docs/layout/index.html index 86ea805a..92c99127 100644 --- a/docs/layout/index.html +++ b/docs/layout/index.html @@ -48,7 +48,7 @@

Light-weight

diff --git a/docs/mithril.redraw.md b/docs/mithril.redraw.md index 64bb4e6a..109028dd 100644 --- a/docs/mithril.redraw.md +++ b/docs/mithril.redraw.md @@ -18,10 +18,65 @@ If you are developing an asynchronous model-level service and finding that Mithr --- +### Changing redraw strategy + +If you need to change how Mithril performs redraws, you can change the value of the `m.redraw.strategy` getter-setter to either `"all"`, `"diff"` or `"none"`. By default, this value is set to `"all"` when running controller constructors, and it's set to `"diff"` for all subsequent redraws. + +```javascript +var module1 = {} +module1.controller = function() { + //this module will attempt to diff its template when routing, as opposed to re-creating the view from scratch. + //this allows config contexts to live across route changes, if its element does not need to be recreated by the diff + m.redraw.strategy("diff") +} +module1.view = function() { + return m("h1", {config: module1.config}, "test") +} +module1.config = function(el, isInit, ctx) { + if (!isInit) ctx.data = "foo" +} +``` + +--- + +### Preventing redraws on events + +Similarly, it's possible to skip redrawing altogether by calling `m.redraw.strategy("none")` + +```javascript +m("input", {onkeydown: function(e) { + if (e.keyCode == 13) ctrl.save() //do things and re-render only if the `enter` key was pressed + else m.redraw.strategy("none") //otherwise, ignore +}}) +``` + +--- + ### Signature [How to read signatures](how-to-read-signatures.md) ```clike -void redraw() -``` \ No newline at end of file +void redraw() { GetterSetter strategy } + +where: + GetterSetter :: String getterSetter([String value]) +``` + +- + + ### m.redraw.strategy + + **GetterSetter strategy** + + The `m.redraw.strategy` getter-setter indicates how the next module redraw will occur. It can be one of three values: + + - `"all"` - recreates the DOM tree from scratch + - `"diff"` - updates only DOM elements if needed + - `"none"` - leaves the DOM tree intact + + This value can be programmatically changed in controllers and event handlers to modify the next redrawing strategy. It is modified internally by Mithril to the value `"all"` before running controller constructors, and to the value `"diff"` after all redraws. + + Calling this function without arguments returns the currently assigned redraw strategy. + + \ No newline at end of file diff --git a/docs/mithril.render.md b/docs/mithril.render.md index bc72d15c..1528ae2b 100644 --- a/docs/mithril.render.md +++ b/docs/mithril.render.md @@ -91,7 +91,7 @@ app.view = function(ctrl) { [How to read signatures](how-to-read-signatures.md) ```clike -void render(DOMElement rootElement, Children children) +void render(DOMElement rootElement, Children children [, Boolean forceRecreation]) where: Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array @@ -112,3 +112,6 @@ where: If it's a list, its contents will recursively be rendered as appropriate and appended as children of the `root` element. +- **Boolean forceRecreation** + + If set to true, rendering a new virtual tree will completely overwrite an existing one without attempting to diff against it \ No newline at end of file diff --git a/mithril.js b/mithril.js index 1dab993d..d8403aef 100644 --- a/mithril.js +++ b/mithril.js @@ -309,6 +309,7 @@ Mithril = m = new function app(window, undefined) { function autoredraw(callback, object, group) { return function(e) { e = e || event + m.redraw.strategy("diff") m.startComputation() try {return callback.call(object, e)} finally { @@ -339,12 +340,13 @@ Mithril = m = new function app(window, undefined) { childNodes: [] } var nodeCache = [], cellCache = {} - m.render = function(root, cell) { + m.render = function(root, cell, forceRecreation) { var configs = [] if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.") var id = getCellCacheKey(root) var node = root == window.document || root == window.document.documentElement ? documentNode : root if (cellCache[id] === undefined) clear(node.childNodes) + if (forceRecreation === true) reset(root) cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs) for (var i = 0; i < configs.length; i++) configs[i]() } @@ -359,6 +361,17 @@ Mithril = m = new function app(window, undefined) { return value } + m.prop = function(store) { + var prop = function() { + if (arguments.length) store = arguments[0] + return store + } + prop.toJSON = function() { + return store + } + return prop + } + var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false m.module = function(root, module) { var index = roots.indexOf(root) @@ -371,6 +384,7 @@ Mithril = m = new function app(window, undefined) { controllers[index].onunload(event) } if (!isPrevented) { + m.redraw.strategy("all") m.startComputation() roots[index] = root modules[index] = module @@ -390,15 +404,18 @@ Mithril = m = new function app(window, undefined) { lastRedrawId = defer(function() {lastRedrawId = null}, 0) } } + m.redraw.strategy = m.prop() function redraw() { + var mode = m.redraw.strategy() for (var i = 0; i < roots.length; i++) { - if (controllers[i]) m.render(roots[i], modules[i].view(controllers[i])) + if (controllers[i] && mode != "none") m.render(roots[i], modules[i].view(controllers[i]), mode == "all") } if (computePostRedrawHook) { computePostRedrawHook() computePostRedrawHook = null } lastRedrawId = null + m.redraw.strategy("diff") } var pendingRequests = 0 @@ -480,7 +497,6 @@ Mithril = m = new function app(window, undefined) { for (var route in router) { if (route == path) { - reset(root) m.module(root, router[route]) return true } @@ -488,7 +504,6 @@ Mithril = m = new function app(window, undefined) { var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") if (matcher.test(path)) { - reset(root) path.replace(matcher, function() { var keys = route.match(/:[^\/]+/g) || [] var values = [].slice.call(arguments, 1, -2) @@ -499,11 +514,6 @@ Mithril = m = new function app(window, undefined) { } } } - function reset(root) { - var cacheKey = getCellCacheKey(root) - clear(root.childNodes, cellCache[cacheKey]) - cellCache[cacheKey] = undefined - } function routeUnobtrusive(e) { e = e || event if (e.ctrlKey || e.metaKey || e.which == 2) return @@ -533,17 +543,10 @@ Mithril = m = new function app(window, undefined) { function decodeSpace(string) { return decodeURIComponent(string.replace(/\+/g, " ")) } - - //model - m.prop = function(store) { - var prop = function() { - if (arguments.length) store = arguments[0] - return store - } - prop.toJSON = function() { - return store - } - return prop + function reset(root) { + var cacheKey = getCellCacheKey(root) + clear(root.childNodes, cellCache[cacheKey]) + cellCache[cacheKey] = undefined } var none = {} diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index 423e5801..770f782e 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -1298,6 +1298,101 @@ function testMithril(mock) { mock.requestAnimationFrame.$resolve() //teardown return unloaded == 1 }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() { + strategy = m.redraw.strategy() + m.redraw.strategy("none") + }, + view: function() { + return m("div"); + } + } + }) + mock.requestAnimationFrame.$resolve() //teardown + return strategy == "all" && root.childNodes.length == 0 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy, count = 0 + var config = function(el, init) {if (!init) count++} + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {}, + view: function() { + return m("div", {config: config}); + } + }, + "/bar1": { + controller: function() { + strategy = m.redraw.strategy() + m.redraw.strategy("redraw") + }, + view: function() { + return m("div", {config: config}); + } + }, + }) + mock.requestAnimationFrame.$resolve() + m.route("/bar1") + mock.requestAnimationFrame.$resolve() //teardown + return strategy == "all" && count == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {this.number = 1}, + view: function(ctrl) { + return m("div", {onclick: function() { + strategy = m.redraw.strategy() + ctrl.number++ + m.redraw.strategy("none") + }}, ctrl.number); + } + } + }) + root.childNodes[0].onclick({}) + return strategy == "diff" && root.childNodes[0].childNodes[0].nodeValue == "1" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var count = 0 + var config = function(el, init ) {if (!init) count++} + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {}, + view: function(ctrl) { + return m("div", {config: config, onclick: function() { + m.redraw.strategy("all") + }}); + } + } + }) + root.childNodes[0].onclick({}) + mock.requestAnimationFrame.$resolve() //teardown + return count == 2 + }) //end m.route //m.prop