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 @@
/*global m */
var app = app || {};
var uniqueId = (function () {
var count = 0;
return function () {
return ++count;
};
}());
// Todo Model
app.Todo = function (data) {
this.title = m.prop(data.title);
this.completed = m.prop(data.completed || false);
this.editing = m.prop(data.editing || false);
this.key = uniqueId();
};
......@@ -51,7 +51,8 @@ app.view = (function () {
classes += task.completed() ? 'completed' : '';
classes += task.editing() ? ' editing' : '';
return classes;
})()
})(),
key: task.key
}, [
m('.view', [
m('input.toggle[type=checkbox]', {
......
......@@ -3,6 +3,7 @@ var m = (function app(window, undefined) {
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 noop = function() {}
// caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
......@@ -33,7 +34,7 @@ var m = (function app(window, undefined) {
*/
function m() {
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 classAttrName = "class" in attrs ? "class" : "className";
var cell = {tag: "div", attrs: {}};
......@@ -48,23 +49,26 @@ var m = (function app(window, undefined) {
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];
if (type.call(children) === ARRAY) {
cell.children = children
var children = hasAttrs ? args.slice(2) : args.slice(1);
if (children.length === 1 && type.call(children[0]) === ARRAY) {
cell.children = children[0]
}
else {
cell.children = hasAttrs ? args.slice(2) : args.slice(1)
cell.children = children
}
for (var attrName in attrs) {
if (attrName === classAttrName) {
if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName];
if (attrs.hasOwnProperty(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
}
function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
......@@ -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
//- 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 = "";
//data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
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) {
......@@ -117,6 +121,7 @@ var m = (function app(window, undefined) {
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
len = data.length
}
}
......@@ -127,18 +132,26 @@ var m = (function app(window, undefined) {
//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
//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;
var existing = {}, shouldMaintainIdentities = false;
for (var i = 0; i < cached.length; i++) {
if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
shouldMaintainIdentities = true;
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 (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
var keysDiffer = false
if (data.length != cached.length) keysDiffer = true
else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
......@@ -161,13 +174,13 @@ var m = (function app(window, undefined) {
element: cached.nodes[existing[key].index] || $document.createElement("div")
}
}
else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
}
}
var actions = []
for (var prop in existing) actions.push(existing[prop])
var changes = actions.sort(sortChanges);
var newCached = new Array(cached.length)
newCached.nodes = cached.nodes.slice()
for (var i = 0, change; change = changes[i]; i++) {
if (change.action === DELETION) {
......@@ -179,6 +192,7 @@ var m = (function app(window, undefined) {
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]})
newCached.nodes[change.index] = dummy
}
if (change.action === MOVE) {
......@@ -186,16 +200,10 @@ var m = (function app(window, undefined) {
parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
}
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.nodes = new Array(parentElement.childNodes.length);
for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
}
}
//end key algorithm
......@@ -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
//the first clause in the regexp matches elements
//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;
cached[cacheCount++] = item
......@@ -231,15 +239,37 @@ var m = (function app(window, undefined) {
}
}
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 (!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 (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.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;
......@@ -247,6 +277,7 @@ var m = (function app(window, undefined) {
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) {
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);
......@@ -259,9 +290,22 @@ var m = (function app(window, undefined) {
data.children,
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 = [];
//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)
}
else {
......@@ -269,6 +313,10 @@ var m = (function app(window, undefined) {
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.nodes.intact = true;
if (controllers.length) {
cached.views = views
cached.controllers = controllers
}
if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
}
//schedule configs to be called. They are called after `build` finishes running
......@@ -284,7 +332,7 @@ var m = (function app(window, undefined) {
configs.push(callback(data, [node, !isNew, context, cached]))
}
}
else if (typeof dataType != FUNCTION) {
else if (typeof data != FUNCTION) {
//handle text nodes
var nodes;
if (cached.nodes.length === 0) {
......@@ -360,7 +408,7 @@ var m = (function app(window, undefined) {
//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
//- 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
if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr
}
......@@ -390,7 +438,15 @@ 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.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop});
}
}
if (cached.children) {
if (type.call(cached.children) === ARRAY) {
for (var i = 0, child; child = cached.children[i]; i++) unload(child)
......@@ -448,7 +504,7 @@ var m = (function app(window, undefined) {
var nodeCache = [], cellCache = {};
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.");
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 isDocumentRoot = root === $document;
var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
......@@ -491,42 +547,75 @@ var m = (function app(window, undefined) {
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
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.");
var index = roots.indexOf(root);
if (index < 0) index = roots.length;
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) {
var event = {
preventDefault: function() {isPrevented = true}
};
controllers[index].onunload(event)
}
if (!isPrevented) {
m.redraw.strategy("all");
m.startComputation();
roots[index] = root;
var currentModule = topModule = module = module || {};
var controller = new (module.controller || function() {});
//controllers may call m.module recursively (via m.route redirects, for example)
//this conditional ensures only the last recursive m.module call is applied
if (currentModule === topModule) {
if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2))
var currentComponent = topComponent = component = component || {controller: function() {}};
var constructor = component.controller || noop
var controller = new constructor;
//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;
modules[index] = module
components[index] = component
}
endFirstComputation();
return controllers[index]
}
};
var redrawing = false
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 null if it's the first redraw and not an event handler
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 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);
lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
}
......@@ -535,14 +624,18 @@ var m = (function app(window, undefined) {
redraw();
lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
}
redrawing = false
};
m.redraw.strategy = m.prop();
var blank = function() {return ""}
function redraw() {
var forceRedraw = m.redraw.strategy() === "all";
if (computePreRedrawHook) {
computePreRedrawHook()
computePreRedrawHook = null
}
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)
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
......@@ -579,7 +672,7 @@ var m = (function app(window, undefined) {
//routing
var modes = {pathname: "", hash: "#", search: "?"};
var redirect = function() {}, routeParams, currentRoute;
var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
m.route = function() {
//m.route()
if (arguments.length === 0) return currentRoute;
......@@ -589,7 +682,10 @@ var m = (function app(window, undefined) {
redirect = function(source) {
var path = currentRoute = normalizeRoute(source);
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)
isDefaultRoute = false
}
};
var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
......@@ -600,19 +696,26 @@ var m = (function app(window, undefined) {
redirect(path)
}
};
computePostRedrawHook = setScroll;
computePreRedrawHook = setScroll;
window[listener]()
}
//config: m.route
else if (arguments[0].addEventListener) {
else if (arguments[0].addEventListener || arguments[0].attachEvent) {
var element = arguments[0];
var isInitialized = arguments[1];
var context = arguments[2];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href;
element.removeEventListener("click", routeUnobtrusive);
element.addEventListener("click", routeUnobtrusive)
var vdom = arguments[3];
element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + vdom.attrs.href;
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) {
var oldRoute = currentRoute;
currentRoute = arguments[0];
......@@ -627,13 +730,16 @@ var m = (function app(window, undefined) {
var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0];
if (window.history.pushState) {
computePreRedrawHook = setScroll
computePostRedrawHook = function() {
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
setScroll()
};
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) {
......@@ -653,9 +759,18 @@ var m = (function app(window, undefined) {
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) {
if (route === path) {
m.module(root, router[route]);
m.mount(root, router[route]);
return true
}
......@@ -666,7 +781,7 @@ var m = (function app(window, undefined) {
var keys = route.match(/:[^\/]+/g) || [];
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])
m.module(root, router[route])
m.mount(root, router[route])
});
return true
}
......@@ -677,8 +792,9 @@ var m = (function app(window, undefined) {
if (e.ctrlKey || e.metaKey || e.which === 2) return;
if (e.preventDefault) e.preventDefault();
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)) : {};
while (currentTarget && currentTarget.nodeName.toUpperCase() != "A") currentTarget = currentTarget.parentNode
m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
}
function setScroll() {
......@@ -686,28 +802,46 @@ var m = (function app(window, undefined) {
else window.scrollTo(0, 0)
}
function buildQueryString(object, prefix) {
var str = [];
for(var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop];
var duplicates = {}
var str = []
for (var prop in object) {
var key = prefix ? prefix + "[" + prop + "]" : prop
var value = object[prop]
var valueType = type.call(value)
var pair = value != null && (valueType === OBJECT) ?
buildQueryString(value, key) :
valueType === ARRAY ?
value.map(function(item) {return encodeURIComponent(key + "[]") + "=" + encodeURIComponent(item)}).join("&") :
encodeURIComponent(key) + "=" + encodeURIComponent(value)
str.push(pair)
var pair = (value === null) ? encodeURIComponent(key) :
valueType === OBJECT ? buildQueryString(value, key) :
valueType === ARRAY ? value.reduce(function(memo, item) {
if (!duplicates[key]) duplicates[key] = {}
if (!duplicates[key][item]) {
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("&")
}
function parseQueryString(str) {
if (str.charAt(0) === "?") str = str.substring(1);
var pairs = str.split("&"), params = {};
for (var i = 0, len = pairs.length; i < len; i++) {
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
}
m.route.buildQueryString = buildQueryString
m.route.parseQueryString = parseQueryString
function reset(root) {
var cacheKey = getCellCacheKey(root);
clear(root.childNodes, cellCache[cacheKey]);
......@@ -719,11 +853,11 @@ var m = (function app(window, undefined) {
deferred.promise = propify(deferred.promise);
return deferred
};
function propify(promise) {
var prop = m.prop();
function propify(promise, initialValue) {
var prop = m.prop(initialValue);
promise.then(prop);
prop.then = function(resolve, reject) {
return propify(promise.then(resolve, reject))
return propify(promise.then(resolve, reject), initialValue)
};
return prop
}
......@@ -976,20 +1110,21 @@ var m = (function app(window, undefined) {
m.request = function(xhrOptions) {
if (xhrOptions.background !== true) m.startComputation();
var deferred = m.deferred();
var deferred = new Deferred();
var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
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
};
xhrOptions.method = (xhrOptions.method || 'GET').toUpperCase();
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
xhrOptions.onload = xhrOptions.onerror = function(e) {
try {
e = e || event;
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 (type.call(response) === ARRAY && xhrOptions.type) {
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) {
if (xhrOptions.background !== true) m.endComputation()
};
ajax(xhrOptions);
deferred.promise(xhrOptions.initialValue);
deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
return deferred.promise
};
......
......@@ -114,7 +114,12 @@
})({});
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 */
......@@ -228,7 +233,7 @@
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
......
{
"private": true,
"dependencies": {
"mithril": "^0.1.20",
"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