Commit 6af87b86 authored by Einar Norðfjörð's avatar Einar Norðfjörð

This fixes a bug in the mithril TodoMVC application

The bug is caused by lack of keying.

To reproduce the bug:
1. Open the app
2. Create at least two tasks
3. Complete all tasks
4. Go to the completed view
5. uncheck the top item
6. Voilá, see how the first item in the list now has an unchecked checkbox
parent aa29541c
...@@ -2,9 +2,17 @@ ...@@ -2,9 +2,17 @@
/*global m */ /*global m */
var app = app || {}; var app = app || {};
var uniqueId = (function () {
var count = 0;
return function () {
return ++count;
};
}());
// Todo Model // Todo Model
app.Todo = function (data) { app.Todo = function (data) {
this.title = m.prop(data.title); this.title = m.prop(data.title);
this.completed = m.prop(data.completed || false); this.completed = m.prop(data.completed || false);
this.editing = m.prop(data.editing || false); this.editing = m.prop(data.editing || false);
this.key = uniqueId();
}; };
...@@ -51,7 +51,8 @@ app.view = (function () { ...@@ -51,7 +51,8 @@ app.view = (function () {
classes += task.completed() ? 'completed' : ''; classes += task.completed() ? 'completed' : '';
classes += task.editing() ? ' editing' : ''; classes += task.editing() ? ' editing' : '';
return classes; return classes;
})() })(),
key: task.key
}, [ }, [
m('.view', [ m('.view', [
m('input.toggle[type=checkbox]', { m('input.toggle[type=checkbox]', {
......
...@@ -3,6 +3,7 @@ var m = (function app(window, undefined) { ...@@ -3,6 +3,7 @@ var m = (function app(window, undefined) {
var type = {}.toString; var type = {}.toString;
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
var noop = function() {}
// caching commonly used variables // caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
...@@ -33,7 +34,7 @@ var m = (function app(window, undefined) { ...@@ -33,7 +34,7 @@ var m = (function app(window, undefined) {
*/ */
function m() { function m() {
var args = [].slice.call(arguments); var args = [].slice.call(arguments);
var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]); var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]);
var attrs = hasAttrs ? args[1] : {}; var attrs = hasAttrs ? args[1] : {};
var classAttrName = "class" in attrs ? "class" : "className"; var classAttrName = "class" in attrs ? "class" : "className";
var cell = {tag: "div", attrs: {}}; var cell = {tag: "div", attrs: {}};
...@@ -48,23 +49,26 @@ var m = (function app(window, undefined) { ...@@ -48,23 +49,26 @@ var m = (function app(window, undefined) {
cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
} }
} }
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
var children = hasAttrs ? args[2] : args[1]; var children = hasAttrs ? args.slice(2) : args.slice(1);
if (type.call(children) === ARRAY) { if (children.length === 1 && type.call(children[0]) === ARRAY) {
cell.children = children cell.children = children[0]
} }
else { else {
cell.children = hasAttrs ? args.slice(2) : args.slice(1) cell.children = children
} }
for (var attrName in attrs) { for (var attrName in attrs) {
if (attrName === classAttrName) { if (attrs.hasOwnProperty(attrName)) {
if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]; if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") {
classes.push(attrs[attrName])
cell.attrs[attrName] = "" //create key in correct iteration order
}
else cell.attrs[attrName] = attrs[attrName]
} }
else cell.attrs[attrName] = attrs[attrName]
} }
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
return cell return cell
} }
function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
...@@ -93,8 +97,8 @@ var m = (function app(window, undefined) { ...@@ -93,8 +97,8 @@ var m = (function app(window, undefined) {
//there's logic that relies on the assumption that null and undefined data are equivalent to empty strings //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 (e.g. function foo() {if (cond) return m("div")} //- 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 //- it simplifies diffing code
//data.toString() is null if data is the return value of Console.log in Firefox //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
if (data == null || data.toString() == null) data = ""; try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""}
if (data.subtree === "retain") return cached; if (data.subtree === "retain") return cached;
var cachedType = type.call(cached), dataType = type.call(data); var cachedType = type.call(cached), dataType = type.call(data);
if (cached == null || cachedType !== dataType) { if (cached == null || cachedType !== dataType) {
...@@ -117,6 +121,7 @@ var m = (function app(window, undefined) { ...@@ -117,6 +121,7 @@ var m = (function app(window, undefined) {
if (type.call(data[i]) === ARRAY) { if (type.call(data[i]) === ARRAY) {
data = data.concat.apply([], data); data = data.concat.apply([], data);
i-- //check current index again and flatten until there are no more nested arrays at that index i-- //check current index again and flatten until there are no more nested arrays at that index
len = data.length
} }
} }
...@@ -127,18 +132,26 @@ var m = (function app(window, undefined) { ...@@ -127,18 +132,26 @@ var m = (function app(window, undefined) {
//2) add new keys to map and mark them for addition //2) add new keys to map and mark them for addition
//3) if key exists in new list, change action from deletion to a move //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 //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 DELETION = 1, INSERTION = 2 , MOVE = 3;
var existing = {}, unkeyed = [], shouldMaintainIdentities = false; var existing = {}, shouldMaintainIdentities = false;
for (var i = 0; i < cached.length; i++) { for (var i = 0; i < cached.length; i++) {
if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
shouldMaintainIdentities = true; shouldMaintainIdentities = true;
existing[cached[i].attrs.key] = {action: DELETION, index: i} existing[cached[i].attrs.key] = {action: DELETION, index: i}
} }
} }
var guid = 0
for (var i = 0, len = data.length; i < len; i++) {
if (data[i] && data[i].attrs && data[i].attrs.key != null) {
for (var j = 0, len = data.length; j < len; j++) {
if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++
}
break
}
}
if (shouldMaintainIdentities) { if (shouldMaintainIdentities) {
if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
var keysDiffer = false var keysDiffer = false
if (data.length != cached.length) keysDiffer = true if (data.length != cached.length) keysDiffer = true
else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
...@@ -161,13 +174,13 @@ var m = (function app(window, undefined) { ...@@ -161,13 +174,13 @@ var m = (function app(window, undefined) {
element: cached.nodes[existing[key].index] || $document.createElement("div") element: cached.nodes[existing[key].index] || $document.createElement("div")
} }
} }
else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
} }
} }
var actions = [] var actions = []
for (var prop in existing) actions.push(existing[prop]) for (var prop in existing) actions.push(existing[prop])
var changes = actions.sort(sortChanges); var changes = actions.sort(sortChanges);
var newCached = new Array(cached.length) var newCached = new Array(cached.length)
newCached.nodes = cached.nodes.slice()
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) {
...@@ -179,6 +192,7 @@ var m = (function app(window, undefined) { ...@@ -179,6 +192,7 @@ var m = (function app(window, undefined) {
dummy.key = data[change.index].attrs.key; dummy.key = data[change.index].attrs.key;
parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
newCached.nodes[change.index] = dummy
} }
if (change.action === MOVE) { if (change.action === MOVE) {
...@@ -186,16 +200,10 @@ var m = (function app(window, undefined) { ...@@ -186,16 +200,10 @@ var m = (function app(window, undefined) {
parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
} }
newCached[change.index] = cached[change.from] newCached[change.index] = cached[change.from]
newCached.nodes[change.index] = change.element
} }
} }
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 = newCached;
cached.nodes = new Array(parentElement.childNodes.length);
for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
} }
} }
//end key algorithm //end key algorithm
...@@ -209,7 +217,7 @@ var m = (function app(window, undefined) { ...@@ -209,7 +217,7 @@ var m = (function app(window, undefined) {
//fix offset of next element if item was a trusted string w/ more than one html element //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 first clause in the regexp matches elements
//the second clause (after the pipe) matches text nodes //the second clause (after the pipe) matches text nodes
subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || []).length subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length
} }
else subArrayCount += type.call(item) === ARRAY ? item.length : 1; else subArrayCount += type.call(item) === ARRAY ? item.length : 1;
cached[cacheCount++] = item cached[cacheCount++] = item
...@@ -231,15 +239,37 @@ var m = (function app(window, undefined) { ...@@ -231,15 +239,37 @@ var m = (function app(window, undefined) {
} }
} }
else if (data != null && dataType === OBJECT) { else if (data != null && dataType === OBJECT) {
var views = [], controllers = []
while (data.view) {
var view = data.view.$original || data.view
var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1
var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop)
var key = data && data.attrs && data.attrs.key
data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"}
if (data.subtree === "retain") return cached;
if (key) {
if (!data.attrs) data.attrs = {}
data.attrs.key = key
}
if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload})
views.push(view)
controllers.push(controller)
}
if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.")
if (!data.attrs) data.attrs = {}; if (!data.attrs) data.attrs = {};
if (!cached.attrs) cached.attrs = {}; if (!cached.attrs) cached.attrs = {};
var dataAttrKeys = Object.keys(data.attrs) var dataAttrKeys = Object.keys(data.attrs)
var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
//if an element is different enough from the one in cache, recreate it //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.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (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.nodes.length) clear(cached.nodes);
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload()
if (cached.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop})
}
}
} }
if (type.call(data.tag) != STRING) return; if (type.call(data.tag) != STRING) return;
...@@ -247,6 +277,7 @@ var m = (function app(window, undefined) { ...@@ -247,6 +277,7 @@ var m = (function app(window, undefined) {
if (data.attrs.xmlns) namespace = data.attrs.xmlns; if (data.attrs.xmlns) namespace = data.attrs.xmlns;
else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; 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"; else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML";
if (isNew) { if (isNew) {
if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); 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); else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag);
...@@ -259,9 +290,22 @@ var m = (function app(window, undefined) { ...@@ -259,9 +290,22 @@ var m = (function app(window, undefined) {
data.children, data.children,
nodes: [node] nodes: [node]
}; };
if (controllers.length) {
cached.views = views
cached.controllers = controllers
for (var i = 0, controller; controller = controllers[i]; i++) {
if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old
if (pendingRequests && controller.onunload) {
var onunload = controller.onunload
controller.onunload = noop
controller.onunload.$old = onunload
}
}
}
if (cached.children && !cached.children.nodes) cached.children.nodes = []; if (cached.children && !cached.children.nodes) cached.children.nodes = [];
//edge case: setting value on <select> doesn't work before children exist, so set it again after children have been created //edge case: setting value on <select> doesn't work before children exist, so set it again after children have been created
if (data.tag === "select" && data.attrs.value) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace); if (data.tag === "select" && "value" in data.attrs) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
parentElement.insertBefore(node, parentElement.childNodes[index] || null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
} }
else { else {
...@@ -269,6 +313,10 @@ var m = (function app(window, undefined) { ...@@ -269,6 +313,10 @@ var m = (function app(window, undefined) {
if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace); if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace);
cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs); cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs);
cached.nodes.intact = true; cached.nodes.intact = true;
if (controllers.length) {
cached.views = views
cached.controllers = controllers
}
if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null) if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
} }
//schedule configs to be called. They are called after `build` finishes running //schedule configs to be called. They are called after `build` finishes running
...@@ -284,7 +332,7 @@ var m = (function app(window, undefined) { ...@@ -284,7 +332,7 @@ var m = (function app(window, undefined) {
configs.push(callback(data, [node, !isNew, context, cached])) configs.push(callback(data, [node, !isNew, context, cached]))
} }
} }
else if (typeof dataType != FUNCTION) { else if (typeof data != FUNCTION) {
//handle text nodes //handle text nodes
var nodes; var nodes;
if (cached.nodes.length === 0) { if (cached.nodes.length === 0) {
...@@ -360,7 +408,7 @@ var m = (function app(window, undefined) { ...@@ -360,7 +408,7 @@ var m = (function app(window, undefined) {
//handle cases that are properties (but ignore cases where we should use setAttribute instead) //handle cases that are properties (but ignore cases where we should use setAttribute instead)
//- list and form are typically used as strings, but are DOM element references in js //- list and form are typically used as strings, but are DOM element references in js
//- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js //- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type")) { else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type" || attrName === "width" || attrName === "height")) {
//#348 don't set the value if not needed otherwise cursor placement breaks in Chrome //#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr
} }
...@@ -390,7 +438,15 @@ var m = (function app(window, undefined) { ...@@ -390,7 +438,15 @@ var m = (function app(window, undefined) {
if (nodes.length != 0) nodes.length = 0 if (nodes.length != 0) nodes.length = 0
} }
function unload(cached) { 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.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop});
}
}
if (cached.children) { if (cached.children) {
if (type.call(cached.children) === ARRAY) { if (type.call(cached.children) === ARRAY) {
for (var i = 0, child; child = cached.children[i]; i++) unload(child) for (var i = 0, child; child = cached.children[i]; i++) unload(child)
...@@ -448,7 +504,7 @@ var m = (function app(window, undefined) { ...@@ -448,7 +504,7 @@ var m = (function app(window, undefined) {
var nodeCache = [], cellCache = {}; var nodeCache = [], cellCache = {};
m.render = function(root, cell, forceRecreation) { m.render = function(root, cell, forceRecreation) {
var configs = []; var configs = [];
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it."); if (!root) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
var id = getCellCacheKey(root); var id = getCellCacheKey(root);
var isDocumentRoot = root === $document; var isDocumentRoot = root === $document;
var node = isDocumentRoot || root === $document.documentElement ? documentNode : root; var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
...@@ -491,42 +547,75 @@ var m = (function app(window, undefined) { ...@@ -491,42 +547,75 @@ var m = (function app(window, undefined) {
return gettersetter(store) return gettersetter(store)
}; };
var roots = [], modules = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePostRedrawHook = null, prevented = false, topModule; var roots = [], components = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePreRedrawHook = null, computePostRedrawHook = null, prevented = false, topComponent, unloaders = [];
var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms
m.module = function(root, module) { function parameterize(component, args) {
var controller = function() {
return (component.controller || noop).apply(this, args) || this
}
var view = function(ctrl) {
if (arguments.length > 1) args = args.concat([].slice.call(arguments, 1))
return component.view.apply(component, args ? [ctrl].concat(args) : [ctrl])
}
view.$original = component.view
var output = {controller: controller, view: view}
if (args[0] && args[0].key != null) output.attrs = {key: args[0].key}
return output
}
m.component = function(component) {
return parameterize(component, [].slice.call(arguments, 1))
}
m.mount = m.module = function(root, component) {
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it."); if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
var index = roots.indexOf(root); var index = roots.indexOf(root);
if (index < 0) index = roots.length; if (index < 0) index = roots.length;
var isPrevented = false; var isPrevented = false;
var event = {preventDefault: function() {
isPrevented = true;
computePreRedrawHook = computePostRedrawHook = null;
}};
for (var i = 0, unloader; unloader = unloaders[i]; i++) {
unloader.handler.call(unloader.controller, event)
unloader.controller.onunload = null
}
if (isPrevented) {
for (var i = 0, unloader; unloader = unloaders[i]; i++) unloader.controller.onunload = unloader.handler
}
else unloaders = []
if (controllers[index] && typeof controllers[index].onunload === FUNCTION) { if (controllers[index] && typeof controllers[index].onunload === FUNCTION) {
var event = {
preventDefault: function() {isPrevented = true}
};
controllers[index].onunload(event) controllers[index].onunload(event)
} }
if (!isPrevented) { if (!isPrevented) {
m.redraw.strategy("all"); m.redraw.strategy("all");
m.startComputation(); m.startComputation();
roots[index] = root; roots[index] = root;
var currentModule = topModule = module = module || {}; if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2))
var controller = new (module.controller || function() {}); var currentComponent = topComponent = component = component || {controller: function() {}};
//controllers may call m.module recursively (via m.route redirects, for example) var constructor = component.controller || noop
//this conditional ensures only the last recursive m.module call is applied var controller = new constructor;
if (currentModule === topModule) { //controllers may call m.mount recursively (via m.route redirects, for example)
//this conditional ensures only the last recursive m.mount call is applied
if (currentComponent === topComponent) {
controllers[index] = controller; controllers[index] = controller;
modules[index] = module components[index] = component
} }
endFirstComputation(); endFirstComputation();
return controllers[index] return controllers[index]
} }
}; };
var redrawing = false
m.redraw = function(force) { m.redraw = function(force) {
if (redrawing) return
redrawing = true
//lastRedrawId is a positive number if a second redraw is requested before the next animation frame //lastRedrawId is a positive number if a second redraw is requested before the next animation frame
//lastRedrawID is null if it's the first redraw and not an event handler //lastRedrawID is null if it's the first redraw and not an event handler
if (lastRedrawId && force !== true) { if (lastRedrawId && force !== true) {
//when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout //when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout
//when rAF: always reschedule redraw //when rAF: always reschedule redraw
if (new Date - lastRedrawCallTime > FRAME_BUDGET || $requestAnimationFrame === window.requestAnimationFrame) { if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) {
if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId); if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId);
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET) lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
} }
...@@ -535,14 +624,18 @@ var m = (function app(window, undefined) { ...@@ -535,14 +624,18 @@ var m = (function app(window, undefined) {
redraw(); redraw();
lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET) lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
} }
redrawing = false
}; };
m.redraw.strategy = m.prop(); m.redraw.strategy = m.prop();
var blank = function() {return ""}
function redraw() { function redraw() {
var forceRedraw = m.redraw.strategy() === "all"; if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
for (var i = 0, root; root = roots[i]; i++) { for (var i = 0, root; root = roots[i]; i++) {
if (controllers[i]) { if (controllers[i]) {
m.render(root, modules[i].view ? modules[i].view(controllers[i]) : blank(), forceRedraw) var args = components[i].controller && components[i].controller.$$args ? [controllers[i]].concat(components[i].controller.$$args) : [controllers[i]]
m.render(root, components[i].view ? components[i].view(controllers[i], args) : "")
} }
} }
//after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState
...@@ -579,7 +672,7 @@ var m = (function app(window, undefined) { ...@@ -579,7 +672,7 @@ var m = (function app(window, undefined) {
//routing //routing
var modes = {pathname: "", hash: "#", search: "?"}; var modes = {pathname: "", hash: "#", search: "?"};
var redirect = function() {}, routeParams, currentRoute; var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
m.route = function() { m.route = function() {
//m.route() //m.route()
if (arguments.length === 0) return currentRoute; if (arguments.length === 0) return currentRoute;
...@@ -589,7 +682,10 @@ var m = (function app(window, undefined) { ...@@ -589,7 +682,10 @@ var m = (function app(window, undefined) {
redirect = function(source) { redirect = function(source) {
var path = currentRoute = normalizeRoute(source); var path = currentRoute = normalizeRoute(source);
if (!routeByValue(root, router, path)) { if (!routeByValue(root, router, path)) {
if (isDefaultRoute) throw new Error("Ensure the default route matches one of the routes defined in m.route")
isDefaultRoute = true
m.route(defaultRoute, true) m.route(defaultRoute, true)
isDefaultRoute = false
} }
}; };
var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate"; var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
...@@ -600,19 +696,26 @@ var m = (function app(window, undefined) { ...@@ -600,19 +696,26 @@ var m = (function app(window, undefined) {
redirect(path) redirect(path)
} }
}; };
computePostRedrawHook = setScroll; computePreRedrawHook = setScroll;
window[listener]() window[listener]()
} }
//config: m.route //config: m.route
else if (arguments[0].addEventListener) { else if (arguments[0].addEventListener || arguments[0].attachEvent) {
var element = arguments[0]; var element = arguments[0];
var isInitialized = arguments[1]; var isInitialized = arguments[1];
var context = arguments[2]; var context = arguments[2];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href; var vdom = arguments[3];
element.removeEventListener("click", routeUnobtrusive); element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + vdom.attrs.href;
element.addEventListener("click", routeUnobtrusive) if (element.addEventListener) {
element.removeEventListener("click", routeUnobtrusive);
element.addEventListener("click", routeUnobtrusive)
}
else {
element.detachEvent("onclick", routeUnobtrusive);
element.attachEvent("onclick", routeUnobtrusive)
}
} }
//m.route(route, params) //m.route(route, params, shouldReplaceHistoryEntry)
else if (type.call(arguments[0]) === STRING) { else if (type.call(arguments[0]) === STRING) {
var oldRoute = currentRoute; var oldRoute = currentRoute;
currentRoute = arguments[0]; currentRoute = arguments[0];
...@@ -627,13 +730,16 @@ var m = (function app(window, undefined) { ...@@ -627,13 +730,16 @@ var m = (function app(window, undefined) {
var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0]; var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0];
if (window.history.pushState) { if (window.history.pushState) {
computePreRedrawHook = setScroll
computePostRedrawHook = function() { computePostRedrawHook = function() {
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute); window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
setScroll()
}; };
redirect(modes[m.route.mode] + currentRoute) redirect(modes[m.route.mode] + currentRoute)
} }
else $location[m.route.mode] = currentRoute else {
$location[m.route.mode] = currentRoute
redirect(modes[m.route.mode] + currentRoute)
}
} }
}; };
m.route.param = function(key) { m.route.param = function(key) {
...@@ -653,9 +759,18 @@ var m = (function app(window, undefined) { ...@@ -653,9 +759,18 @@ var m = (function app(window, undefined) {
path = path.substr(0, queryStart) path = path.substr(0, queryStart)
} }
// Get all routes and check if there's
// an exact match for the current path
var keys = Object.keys(router);
var index = keys.indexOf(path);
if(index !== -1){
m.mount(root, router[keys [index]]);
return true;
}
for (var route in router) { for (var route in router) {
if (route === path) { if (route === path) {
m.module(root, router[route]); m.mount(root, router[route]);
return true return true
} }
...@@ -666,7 +781,7 @@ var m = (function app(window, undefined) { ...@@ -666,7 +781,7 @@ var m = (function app(window, undefined) {
var keys = route.match(/:[^\/]+/g) || []; var keys = route.match(/:[^\/]+/g) || [];
var values = [].slice.call(arguments, 1, -2); var values = [].slice.call(arguments, 1, -2);
for (var i = 0, len = keys.length; i < len; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) for (var i = 0, len = keys.length; i < len; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
m.module(root, router[route]) m.mount(root, router[route])
}); });
return true return true
} }
...@@ -677,8 +792,9 @@ var m = (function app(window, undefined) { ...@@ -677,8 +792,9 @@ var m = (function app(window, undefined) {
if (e.ctrlKey || e.metaKey || e.which === 2) return; if (e.ctrlKey || e.metaKey || e.which === 2) return;
if (e.preventDefault) e.preventDefault(); if (e.preventDefault) e.preventDefault();
else e.returnValue = false; else e.returnValue = false;
var currentTarget = e.currentTarget || this; var currentTarget = e.currentTarget || e.srcElement;
var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {}; var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
while (currentTarget && currentTarget.nodeName.toUpperCase() != "A") currentTarget = currentTarget.parentNode
m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args) m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
} }
function setScroll() { function setScroll() {
...@@ -686,28 +802,46 @@ var m = (function app(window, undefined) { ...@@ -686,28 +802,46 @@ var m = (function app(window, undefined) {
else window.scrollTo(0, 0) else window.scrollTo(0, 0)
} }
function buildQueryString(object, prefix) { function buildQueryString(object, prefix) {
var str = []; var duplicates = {}
for(var prop in object) { var str = []
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop]; for (var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop
var value = object[prop]
var valueType = type.call(value) var valueType = type.call(value)
var pair = value != null && (valueType === OBJECT) ? var pair = (value === null) ? encodeURIComponent(key) :
buildQueryString(value, key) : valueType === OBJECT ? buildQueryString(value, key) :
valueType === ARRAY ? valueType === ARRAY ? value.reduce(function(memo, item) {
value.map(function(item) {return encodeURIComponent(key + "[]") + "=" + encodeURIComponent(item)}).join("&") : if (!duplicates[key]) duplicates[key] = {}
encodeURIComponent(key) + "=" + encodeURIComponent(value) if (!duplicates[key][item]) {
str.push(pair) duplicates[key][item] = true
return memo.concat(encodeURIComponent(key) + "=" + encodeURIComponent(item))
}
return memo
}, []).join("&") :
encodeURIComponent(key) + "=" + encodeURIComponent(value)
if (value !== undefined) str.push(pair)
} }
return str.join("&") return str.join("&")
} }
function parseQueryString(str) { function parseQueryString(str) {
if (str.charAt(0) === "?") str = str.substring(1);
var pairs = str.split("&"), params = {}; var pairs = str.split("&"), params = {};
for (var i = 0, len = pairs.length; i < len; i++) { for (var i = 0, len = pairs.length; i < len; i++) {
var pair = pairs[i].split("="); var pair = pairs[i].split("=");
params[decodeURIComponent(pair[0])] = pair[1] ? decodeURIComponent(pair[1]) : "" var key = decodeURIComponent(pair[0])
var value = pair.length == 2 ? decodeURIComponent(pair[1]) : null
if (params[key] != null) {
if (type.call(params[key]) !== ARRAY) params[key] = [params[key]]
params[key].push(value)
}
else params[key] = value
} }
return params return params
} }
m.route.buildQueryString = buildQueryString
m.route.parseQueryString = parseQueryString
function reset(root) { function reset(root) {
var cacheKey = getCellCacheKey(root); var cacheKey = getCellCacheKey(root);
clear(root.childNodes, cellCache[cacheKey]); clear(root.childNodes, cellCache[cacheKey]);
...@@ -719,11 +853,11 @@ var m = (function app(window, undefined) { ...@@ -719,11 +853,11 @@ var m = (function app(window, undefined) {
deferred.promise = propify(deferred.promise); deferred.promise = propify(deferred.promise);
return deferred return deferred
}; };
function propify(promise) { function propify(promise, initialValue) {
var prop = m.prop(); var prop = m.prop(initialValue);
promise.then(prop); promise.then(prop);
prop.then = function(resolve, reject) { prop.then = function(resolve, reject) {
return propify(promise.then(resolve, reject)) return propify(promise.then(resolve, reject), initialValue)
}; };
return prop return prop
} }
...@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) { ...@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) {
m.request = function(xhrOptions) { m.request = function(xhrOptions) {
if (xhrOptions.background !== true) m.startComputation(); if (xhrOptions.background !== true) m.startComputation();
var deferred = m.deferred(); var deferred = new Deferred();
var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp"; var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify; var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse; var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse;
var extract = xhrOptions.extract || function(xhr) { var extract = isJSONP ? function(jsonp) {return jsonp.responseText} : xhrOptions.extract || function(xhr) {
return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText
}; };
xhrOptions.method = (xhrOptions.method || 'GET').toUpperCase();
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data); xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize); xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
xhrOptions.onload = xhrOptions.onerror = function(e) { xhrOptions.onload = xhrOptions.onerror = function(e) {
try { try {
e = e || event; e = e || event;
var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity; var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
var response = unwrap(deserialize(extract(e.target, xhrOptions))); var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target);
if (e.type === "load") { if (e.type === "load") {
if (type.call(response) === ARRAY && xhrOptions.type) { if (type.call(response) === ARRAY && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i]) for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
...@@ -1005,7 +1140,7 @@ var m = (function app(window, undefined) { ...@@ -1005,7 +1140,7 @@ var m = (function app(window, undefined) {
if (xhrOptions.background !== true) m.endComputation() if (xhrOptions.background !== true) m.endComputation()
}; };
ajax(xhrOptions); ajax(xhrOptions);
deferred.promise(xhrOptions.initialValue); deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
return deferred.promise return deferred.promise
}; };
......
...@@ -114,7 +114,12 @@ ...@@ -114,7 +114,12 @@
})({}); })({});
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
} }
/* jshint ignore:end */ /* jshint ignore:end */
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
xhr.onload = function (e) { xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText); var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) { if (parsedResponse instanceof Array) {
var count = parsedResponse.length var count = parsedResponse.length;
if (count !== 0) { if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues'; issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline'; document.getElementById('issue-count').style.display = 'inline';
......
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"mithril": "^0.1.20",
"todomvc-common": "^1.0.1", "todomvc-common": "^1.0.1",
"todomvc-app-css": "^1.0.1" "todomvc-app-css": "^1.0.1",
"mithril": "~0.2.0"
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment