diff --git a/docs/mithril.redraw.md b/docs/mithril.redraw.md index 16c8f504..344620b4 100644 --- a/docs/mithril.redraw.md +++ b/docs/mithril.redraw.md @@ -122,7 +122,7 @@ Note that the redraw strategy is a global setting that affects the entire templa ### Preventing redraws on events -Sometimes you only care about a particular condition in an event and want to ignore it if this condition is not met. +Sometimes you only care about a particular condition in an event and want the event to not trigger a redraw if this condition is not met. For example, you might only be interested in running a redraw if a user presses the space bar, and you might not want to waste a redraw if the user presses any other key. In that case, it's possible to skip redrawing altogether by calling `m.redraw.strategy("none")` ```javascript @@ -132,6 +132,14 @@ m("input", {onkeydown: function(e) { }}) ``` +There are some important semantic caveats for `m.redraw.strategy("none")` that you should be aware of: Setting the strategy to `"none"` only affects **synchronous** redraws. As soon as the event handler returns, the strategy is set back to "diff". + +If you set strategy to `"none"` but then proceed to trigger a redraw asynchronously, either via `start/endComputation`, `m.redraw` or `m.request`, a redraw *will* occur, using the `"diff"` strategy. + +Additionally, calling `m.redraw` synchronously after calling `m.redraw.strategy("none")` resets the strategy to `"diff"`. + +Lastly, be aware that if a user action triggers more than one event handler (for example, oninput and onkeypress, or an event bubbling up to event handlers in multiple ancestor elements), every event triggers a redraw by default. Setting strategy to none in any one of those handlers will not affect the redrawing strategy of other handlers (and remember that `strategy("none")` has no effect on asynchronous redraws). + --- ### Forcing redraw diff --git a/mithril.js b/mithril.js index 64c9ef55..867d4ed7 100644 --- a/mithril.js +++ b/mithril.js @@ -1,8 +1,22 @@ -Mithril = m = new function app(window, undefined) { - var sObj = "[object Object]", sArr = "[object Array]", sStr = "[object String]", sFn = "function" - var type = {}.toString - var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ - var voidElements = /AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR/ +var m = (function app(window, undefined) { + var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; + var type = {}.toString; + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; + var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; + + // caching commonly used variables + var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; + + // self invoking function needed because of the way mocks work + function initialize(window){ + $document = window.document; + $location = window.location; + $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; + $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; + } + + initialize(window); + /* * @typedef {String} Tag @@ -18,26 +32,27 @@ Mithril = m = new function app(window, undefined) { * */ function m() { - var args = [].slice.call(arguments) - var hasAttrs = args[1] != null && type.call(args[1]) == sObj && !("tag" in args[1]) && !("subtree" in args[1]) - var attrs = hasAttrs ? args[1] : {} - var classAttrName = "class" in attrs ? "class" : "className" - var cell = {tag: "div", attrs: {}} - var match, classes = [] + var args = [].slice.call(arguments); + var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]); + var attrs = hasAttrs ? args[1] : {}; + var classAttrName = "class" in attrs ? "class" : "className"; + var cell = {tag: "div", attrs: {}}; + var match, classes = []; + if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") while (match = parser.exec(args[0])) { - if (match[1] == "" && match[2]) cell.tag = match[2] - else if (match[1] == "#") cell.attrs.id = match[2] - else if (match[1] == ".") classes.push(match[2]) - else if (match[3][0] == "[") { - var pair = attrParser.exec(match[3]) + if (match[1] === "" && match[2]) cell.tag = match[2]; + else if (match[1] === "#") cell.attrs.id = match[2]; + else if (match[1] === ".") classes.push(match[2]); + else if (match[3][0] === "[") { + var pair = attrParser.exec(match[3]); cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) } } - if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); - var children = hasAttrs ? args[2] : args[1] - if (type.call(children) == sArr) { + var children = hasAttrs ? args[2] : args[1]; + if (type.call(children) === ARRAY) { cell.children = children } else { @@ -45,7 +60,7 @@ Mithril = m = new function app(window, undefined) { } for (var attrName in attrs) { - if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName] + if (attrName === classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]; else cell.attrs[attrName] = attrs[attrName] } return cell @@ -74,28 +89,35 @@ Mithril = m = new function app(window, undefined) { //`configs` is a list of config functions to run after the topmost `build` call finishes running //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings - //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements + //- 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 - if (data == null) data = "" - if (data.subtree === "retain") return cached - var cachedType = type.call(cached), dataType = type.call(data) - if (cached == null || cachedType != dataType) { + if (data == null || data.toString() == null) data = ""; + if (data.subtree === "retain") return cached; + var cachedType = type.call(cached), dataType = type.call(data); + if (cached == null || cachedType !== dataType) { if (cached != null) { if (parentCache && parentCache.nodes) { - var offset = index - parentIndex - var end = offset + (dataType == sArr ? data : cached.nodes).length + var offset = index - parentIndex; + var end = offset + (dataType === ARRAY ? data : cached.nodes).length; clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) } else if (cached.nodes) clear(cached.nodes, cached) } - cached = new data.constructor - if (cached.tag) cached = {} //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) + cached = new data.constructor; + if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) cached.nodes = [] } - if (dataType == sArr) { - data = flatten(data) - var nodes = [], intact = cached.length === data.length, subArrayCount = 0 + if (dataType === ARRAY) { + //recursively flatten array + for (var i = 0, len = data.length; i < len; i++) { + if (type.call(data[i]) === ARRAY) { + data = data.concat.apply([], data); + i-- //check current index again and flatten until there are no more nested arrays at that index + } + } + + var nodes = [], intact = cached.length === data.length, subArrayCount = 0; //keys algorithm: sort elements without recreating them if keys are present //1) create a map of all existing keys, and mark all for deletion @@ -103,116 +125,115 @@ Mithril = m = new function app(window, undefined) { //3) if key exists in new list, change action from deletion to a move //4) for each key, handle its corresponding action as marked in previous steps //5) copy unkeyed items into their respective gaps - var DELETION = 1, INSERTION = 2 , MOVE = 3 - var existing = {}, unkeyed = [], shouldMaintainIdentities = false - for (var i = 0; i < cached.length; i++) { + var DELETION = 1, INSERTION = 2 , MOVE = 3; + var existing = {}, unkeyed = [], shouldMaintainIdentities = false; + for (var i = 0, len = cached.length; i < len; i++) { if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { - shouldMaintainIdentities = true + shouldMaintainIdentities = true; existing[cached[i].attrs.key] = {action: DELETION, index: i} } } if (shouldMaintainIdentities) { - for (var i = 0; i < data.length; i++) { + if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null}) + for (var i = 0, len = data.length; i < len; i++) { if (data[i] && data[i].attrs) { if (data[i].attrs.key != null) { - var key = data[i].attrs.key - if (!existing[key]) existing[key] = {action: INSERTION, index: i} + var key = data[i].attrs.key; + if (!existing[key]) existing[key] = {action: INSERTION, index: i}; else existing[key] = { action: MOVE, index: i, from: existing[key].index, - element: parentElement.childNodes[existing[key].index] || window.document.createElement("div") + element: parentElement.childNodes[existing[key].index] || $document.createElement("div") } } - else unkeyed.push({index: i, element: parentElement.childNodes[i] || window.document.createElement("div")}) + else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")}) } } - var actions = Object.keys(existing).map(function(key) {return existing[key]}) - var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index}) - var newCached = cached.slice() + var actions = Object.keys(existing).map(function(key) {return existing[key]}); + var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index}); + var newCached = cached.slice(); for (var i = 0, change; change = changes[i]; i++) { - if (change.action == DELETION) { - clear(cached[change.index].nodes, cached[change.index]) + if (change.action === DELETION) { + clear(cached[change.index].nodes, cached[change.index]); newCached.splice(change.index, 1) } - if (change.action == INSERTION) { - var dummy = window.document.createElement("div") - dummy.key = data[change.index].attrs.key - parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null) + if (change.action === INSERTION) { + var dummy = $document.createElement("div"); + dummy.key = data[change.index].attrs.key; + parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) } - if (change.action == MOVE) { + if (change.action === MOVE) { if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) } newCached[change.index] = cached[change.from] } } - for (var i = 0; i < unkeyed.length; i++) { - var change = unkeyed[i] - parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) + for (var i = 0, len = unkeyed.length; i < len; i++) { + var change = unkeyed[i]; + parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null); newCached[change.index] = cached[change.index] } - cached = newCached - cached.nodes = [] + cached = newCached; + cached.nodes = []; for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child) } //end key algorithm - for (var i = 0, cacheCount = 0; i < data.length; i++) { + for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { //diff each item in the array - var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) - if (item === undefined) continue - if (!item.nodes.intact) intact = false + var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); + if (item === undefined) continue; + if (!item.nodes.intact) intact = false; if (item.$trusted) { //fix offset of next element if item was a trusted string w/ more than one html element //the first clause in the regexp matches elements //the second clause (after the pipe) matches text nodes subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || []).length } - else subArrayCount += type.call(item) == sArr ? item.length : 1 + else subArrayCount += type.call(item) === ARRAY ? item.length : 1; cached[cacheCount++] = item } if (!intact) { //diff the array itself - + //update the list of DOM nodes by collecting the nodes from each item - for (var i = 0; i < data.length; i++) { - if (cached[i] != null) nodes = nodes.concat(cached[i].nodes) + for (var i = 0, len = data.length; i < len; i++) { + if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) } //remove items from the end of the array if the new array is shorter than the old one //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program for (var i = 0, node; node = cached.nodes[i]; i++) { if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) } - //add items to the end if the new array is longer than the old one - for (var i = cached.nodes.length, node; node = nodes[i]; i++) { - if (node.parentNode == null) parentElement.appendChild(node) - } - if (data.length < cached.length) cached.length = data.length + if (data.length < cached.length) cached.length = data.length; cached.nodes = nodes } } - else if (data != null && dataType == sObj) { - if (!data.attrs) data.attrs = {} - if (!cached.attrs) cached.attrs = {} - - var dataAttrKeys = Object.keys(data.attrs) + else if (data != null && dataType === OBJECT) { + if (!data.attrs) data.attrs = {}; + if (!cached.attrs) cached.attrs = {}; + + 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 (cached.nodes.length) clear(cached.nodes) - if (cached.configContext && typeof cached.configContext.onunload == sFn) cached.configContext.onunload() + if (cached.nodes.length) clear(cached.nodes); + if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() } - if (type.call(data.tag) != sStr) return + if (type.call(data.tag) != STRING) return; - var node, isNew = cached.nodes.length === 0 - if (data.attrs.xmlns) namespace = data.attrs.xmlns - else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg" - else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML" + var node, isNew = cached.nodes.length === 0; + if (data.attrs.xmlns) namespace = data.attrs.xmlns; + else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; + else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; if (isNew) { - node = namespace === undefined ? window.document.createElement(data.tag) : window.document.createElementNS(namespace, data.tag) + if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); + else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); cached = { tag: data.tag, //set attributes first, then create children @@ -221,69 +242,69 @@ Mithril = m = new function app(window, undefined) { build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : data.children, nodes: [node] - } - if (cached.children && !cached.children.nodes) cached.children.nodes = [] + }; + if (cached.children && !cached.children.nodes) cached.children.nodes = []; //edge case: setting value on