diff --git a/mithril.js b/mithril.js index d8403aef..187febf0 100644 --- a/mithril.js +++ b/mithril.js @@ -1,7 +1,7 @@ Mithril = m = new function app(window, undefined) { var type = {}.toString var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ - + function m() { var args = arguments var hasAttrs = args[1] !== undefined && type.call(args[1]) == "[object Object]" && !("tag" in args[1]) && !("subtree" in args[1]) @@ -19,9 +19,9 @@ Mithril = m = new function app(window, undefined) { } } if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") - + cell.children = hasAttrs ? args[2] : args[1] - + for (var attrName in attrs) { if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName] else cell.attrs[attrName] = attrs[attrName] @@ -40,7 +40,7 @@ Mithril = m = new function app(window, undefined) { //`editable` is a flag that indicates whether an ancestor is contenteditable //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor //`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 //- it simplifies diffing code @@ -64,7 +64,7 @@ Mithril = m = new function app(window, undefined) { if (dataType == "[object Array]") { data = flatten(data) var nodes = [], intact = cached.length === data.length, subArrayCount = 0 - + //key algorithm: sort elements without recreating them if keys are present //1) create a map of all existing keys, and mark all for deletion //2) add new keys to map and mark them for addition @@ -93,7 +93,7 @@ Mithril = m = new function app(window, undefined) { 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]) @@ -105,7 +105,7 @@ Mithril = m = new function app(window, undefined) { parentElement.insertBefore(dummy, parentElement.childNodes[change.index]) newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) } - + if (change.action == MOVE) { if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) @@ -123,7 +123,7 @@ Mithril = m = new function app(window, undefined) { 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++) { var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) if (item === undefined) continue @@ -145,7 +145,7 @@ Mithril = m = new function app(window, undefined) { if (data.length < cached.length) cached.length = data.length cached.nodes = nodes } - + } else if (data !== undefined && dataType == "[object Object]") { //if an element is different enough from the one in cache, recreate it @@ -361,17 +361,31 @@ Mithril = m = new function app(window, undefined) { return value } - m.prop = function(store) { + function _prop(store) { var prop = function() { if (arguments.length) store = arguments[0] return store } + prop.toJSON = function() { return store } + return prop } - + + m.prop = function (store) { + if ((typeof store === 'object' || typeof store === 'function') && + typeof store.then === 'function') { + var prop = _prop() + newPromisedProp(prop, store).then(prop) + + return prop + } + + return _prop(store) + } + var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false m.module = function(root, module) { var index = roots.indexOf(root) @@ -392,10 +406,10 @@ Mithril = m = new function app(window, undefined) { m.endComputation() } } - m.redraw = function() { + m.redraw = function(force) { var cancel = window.cancelAnimationFrame || window.clearTimeout var defer = window.requestAnimationFrame || window.setTimeout - if (lastRedrawId) { + if (lastRedrawId && !force) { cancel(lastRedrawId) lastRedrawId = defer(redraw, 0) } @@ -472,7 +486,7 @@ Mithril = m = new function app(window, undefined) { if (querystring) currentRoute += (currentRoute.indexOf("?") === -1 ? "?" : "&") + querystring var shouldReplaceHistoryEntry = (arguments.length == 3 ? arguments[2] : arguments[1]) === true - + if (window.history.pushState) { computePostRedrawHook = function() { window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + currentRoute) @@ -550,48 +564,155 @@ Mithril = m = new function app(window, undefined) { } var none = {} - m.deferred = function() { - var resolvers = [], rejecters = [], resolved = none, rejected = none, promise = m.prop() - var object = { - resolve: function(value) { - if (resolved === none) promise(resolved = value) - for (var i = 0; i < resolvers.length; i++) resolvers[i](value) - resolvers.length = rejecters.length = 0 - }, - reject: function(value) { - if (rejected === none) rejected = value - for (var i = 0; i < rejecters.length; i++) rejecters[i](value) - resolvers.length = rejecters.length = 0 - }, - promise: promise + function newPromisedProp(prop, promise) { + prop.then = function () { + var newProp = m.prop() + return newPromisedProp(newProp, + promise.then.apply(promise, arguments).then(newProp)) } - object.promise.resolvers = resolvers - object.promise.then = function(success, error) { - var next = m.deferred() - if (!success) success = identity - if (!error) error = identity - function callback(method, callback) { - return function(value) { - try { - var result = callback(value) - if (result && typeof result.then == "function") result.then(next[method], error) - else next[method](result !== undefined ? result : value) - } - catch (e) { - if (type.call(e) == "[object Error]" && e.constructor !== Error) throw e - else next.reject(e) - } + prop.promise = prop + prop.resolve = function (val) { + prop(val) + promise = promise.resolve.apply(promise, arguments) + return prop + } + prop.reject = function () { + promise = promise.reject.apply(promise, arguments) + return prop + } + + return prop + } + m.deferred = function () { + + // Promiz.mithril.js | Zolmeister | MIT + function Deferred(fn, er) { + // states + // 0: pending + // 1: resolving + // 2: rejecting + // 3: resolved + // 4: rejected + var self = this, + state = 0, + val = 0, + next = []; + + self['promise'] = self + + self['resolve'] = function (v) { + if (!state) { + val = v + state = 1 + + fire() } + return this } - if (resolved !== none) callback("resolve", success)(resolved) - else if (rejected !== none) callback("reject", error)(rejected) - else { - resolvers.push(callback("resolve", success)) - rejecters.push(callback("reject", error)) + + self['reject'] = function (v) { + if (!state) { + val = v + state = 2 + + fire() + } + return this + } + + self['then'] = function (fn, er) { + var d = new Deferred(fn, er) + if (state == 3) { + d.resolve(val) + } + else if (state == 4) { + d.reject(val) + } + else { + next.push(d) + } + return d + } + + var finish = function (type) { + state = type || 4 + next.map(function (p) { + state == 3 && p.resolve(val) || p.reject(val) + }) + } + + // ref : reference to 'then' function + // cb, ec, cn : successCallback, failureCallback, notThennableCallback + function thennable (ref, cb, ec, cn) { + if ((typeof val == 'object' || typeof val == 'function') && typeof ref == 'function') { + try { + + // cnt protects against abuse calls from spec checker + var cnt = 0 + ref.call(val, function (v) { + if (cnt++) return + val = v + cb() + }, function (v) { + if (cnt++) return + val = v + ec() + }) + } catch (e) { + val = e + ec() + } + } else { + cn() + } + }; + + function fire() { + + // check if it's a thenable + var ref; + try { + ref = val && val.then + } catch (e) { + val = e + state = 2 + return fire() + } + thennable(ref, function () { + state = 1 + fire() + }, function () { + state = 2 + fire() + }, function () { + try { + if (state == 1 && typeof fn == 'function') { + val = fn(val) + } + + else if (state == 2 && typeof er == 'function') { + val = er(val) + state = 1 + } + } catch (e) { + val = e + return finish() + } + + if (val == self) { + val = TypeError() + finish() + } else thennable(ref, function () { + finish(3) + }, finish, function () { + finish(state == 1 && 3) + }) + + }) } - return next.promise } - return object + + return newPromisedProp(m.prop(), new Deferred()) } m.sync = function(args) { var method = "resolve" @@ -616,7 +737,7 @@ Mithril = m = new function app(window, undefined) { } } else deferred.resolve() - + return deferred.promise } function identity(value) {return value} diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index 770f782e..3de5fe91 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -736,13 +736,32 @@ function testMithril(mock) { }) mock.requestAnimationFrame.$resolve() //teardown m.redraw() //should run synchronously - + m.redraw() //rest should run asynchronously since they're spamming m.redraw() m.redraw() mock.requestAnimationFrame.$resolve() //teardown return count === 3 }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + var count = 0 + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {}, + view: function(ctrl) { + count++ + } + }) + mock.requestAnimationFrame.$resolve() //teardown + m.redraw(true) //should run synchronously + + m.redraw(true) //forced to run synchronously + m.redraw(true) + m.redraw(true) + mock.requestAnimationFrame.$resolve() //teardown + return count === 5 + }) //m.route test(function() { @@ -1413,6 +1432,22 @@ function testMithril(mock) { var obj = {prop: m.prop("test")} return JSON.stringify(obj) === '{"prop":"test"}' }) + test(function() { + var defer = m.deferred() + var prop = m.prop(defer.promise) + defer.resolve("test") + + return prop() === "test" + }) + test(function() { + var defer = m.deferred() + var prop = m.prop(defer.promise).then(function () { + return "test2" + }) + defer.resolve("test") + + return prop() === "test2" + }) //m.request test(function() { @@ -1439,7 +1474,7 @@ function testMithril(mock) { var error = m.prop("no error") var prop = m.request({method: "GET", url: "test", deserialize: function() {throw new Error("error occurred")}}).then(null, error) mock.XMLHttpRequest.$instances.pop().onreadystatechange() - return prop() === undefined && error().message === "error occurred" + return prop().message === "error occurred" && error().message === "error occurred" }) test(function() { var error = m.prop("no error"), exception @@ -1482,7 +1517,7 @@ function testMithril(mock) { test(function() { var value var deferred = m.deferred() - deferred.promise.then(null, function(data) {return "foo"}).then(null, function(data) {value = data}) + deferred.promise.then(null, function(data) {return "foo"}).then(function(data) {value = data}) deferred.reject("test") return value === "foo" }) @@ -1656,7 +1691,7 @@ function testMithril(mock) { controller.value = "foo" m.endComputation() mock.requestAnimationFrame.$resolve() - + return root.childNodes[0].nodeValue === "foo" }) diff --git a/tests/test.js b/tests/test.js index 025d8216..a07a82b1 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,35 +1,40 @@ function test(condition) { - var duration = 0; - var start = 0; - var result = true; + var duration = 0 + var start = 0 + var result = true test.total++ + if (typeof performance != "undefined") { + start = performance.now() + } + try { + if (!condition()) throw new Error() + } + catch (e) { + result = false + console.error(e) + test.failures.push(condition) + } + if (typeof performance != "undefined") { + duration = performance.now() - start + } + + test_obj = { + name: "" + test.total, + result: result, + duration: duration + } + if (typeof window != "undefined") { - if (typeof performance != "undefined") { - start = performance.now(); - } - try {if (!condition()) throw new Error} - catch (e) {result = false;console.error(e);test.failures.push(condition)} - if (typeof performance != "undefined") { - duration = performance.now() - start; - } - - - test_obj = { - name: "" + test.total, - result: result, - duration: duration - } if (!result) { - message: "failed: " + condition, window.global_test_results.tests.push(test_obj) } - window.global_test_results.duration += duration; + window.global_test_results.duration += duration if (result) { - window.global_test_results.passed++; + window.global_test_results.passed++ } else { - window.global_test_results.failed++; + window.global_test_results.failed++ } } }