diff --git a/mithril.js b/mithril.js index ecaef3f2..17c50155 100644 --- a/mithril.js +++ b/mithril.js @@ -1,4 +1,100 @@ "use strict" +if (typeof Promise === "undefined") { + var Promise = function(executor) { + if (!(this instanceof Promise)) throw new Error("Promise must be called with `new`") + if (typeof executor !== "function") throw new TypeError("executor must be a function") + + var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) + var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} + var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout + function handler(list, shouldAbsorb) { + return function execute(value) { + var then + try { + if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { + if (value === self) throw new TypeError("Promise can't be resolved w/ itself") + executeOnce(then.bind(value)) + } + else { + callAsync(function() { + if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value) + for (var i = 0; i < list.length; i++) list[i](value) + resolvers.length = 0, rejectors.length = 0 + instance.state = shouldAbsorb + instance.retry = function() {execute(value)} + }, 0) + } + } + catch (e) { + rejectCurrent(e) + } + } + } + function executeOnce(then) { + var runs = 0 + function run(fn) { + return function(value) { + if (runs++ > 0) return + fn(value) + } + } + var onerror = run(rejectCurrent) + try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)} + } + + executeOnce(executor) + } + Promise.prototype.then = function(onFulfilled, onRejection) { + var self = this, instance = self._instance + function handle(callback, list, next, state) { + list.push(function(value) { + if (typeof callback !== "function") next(value) + else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} + }) + if (typeof instance.retry === "function" && state === instance.state) instance.retry() + } + var resolveNext, rejectNext + var promise = new Promise(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) + return promise + } + Promise.prototype.catch = function(onRejection) { + return this.then(null, onRejection) + } + Promise.resolve = function(value) { + if (value instanceof Promise) return value + return new Promise(function(resolve, reject) {resolve(value)}) + } + Promise.reject = function(value) { + return new Promise(function(resolve, reject) {reject(value)}) + } + Promise.all = function(list) { + return new Promise(function(resolve, reject) { + var total = list.length, count = 0, values = [] + if (list.length === 0) resolve([]) + else for (var i = 0; i < list.length; i++) { + new function(i) { + function consume(value) { + count++ + values[i] = value + if (count === total) resolve(values) + } + if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { + list[i].then(consume, reject) + } + else consume(list[i]) + }(i) + } + }) + } + Promise.race = function(list) { + return new Promise(function(resolve, reject) { + for (var i = 0; i < list.length; i++) { + list[i].then(resolve, reject) + } + }) + } +} function Node(tag, key, attrs, children, text, dom) { return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined} } @@ -13,7 +109,7 @@ Node.normalizeChildren = function normalizeChildren(children) { } return children } -var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:\s*=\s*("|'|)(.*?)\2)?\]/ +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorCache = {} function hyperscript(selector) { if (typeof selector === "string") { @@ -25,8 +121,9 @@ function hyperscript(selector) { else if (type === "#") attributes.id = value else if (type === ".") classes.push(value) else if (match[3][0] === "[") { - var pair = attrParser.exec(match[3]) - attributes[pair[1]] = pair[3] || true + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") + attributes[match[4]] = attrValue || true } } if (classes.length > 0) attributes.className = classes.join(" ") @@ -601,10 +698,10 @@ var buildQueryString = function(object) { else args.push(encodeURIComponent(key) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) } } -var requestService = function($window, Promise) { +var requestService = function($window, Promise1) { var callbackCount = 0 function xhr(args) { - return new Promise(function(resolve, reject) { + return new Promise1(function(resolve, reject) { var useBody = args.useBody != null ? args.useBody : args.method !== "GET" && args.method !== "TRACE" if (typeof args.serialize !== "function") args.serialize = JSON.stringify @@ -656,7 +753,7 @@ var requestService = function($window, Promise) { }) } function jsonp(args) { - return new Promise(function(resolve, reject) { + return new Promise1(function(resolve, reject) { var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ var script = $window.document.createElement("script") $window[callbackName] = function(data) { diff --git a/render/render.js b/render/render.js index 216454b1..bd0778d0 100644 --- a/render/render.js +++ b/render/render.js @@ -325,7 +325,7 @@ module.exports = function($window) { } } if (vnode.dom.parentNode != null) parent.removeChild(vnode.dom) - if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode)) { //TODO test custom elements + if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && !(typeof vnode.tag !== "string" && hasIntegrationMethods(vnode.tag))) { //TODO test custom elements if (!context.pool) context.pool = [vnode] else context.pool.push(vnode) } @@ -352,7 +352,7 @@ module.exports = function($window) { } function setAttr(vnode, key, old, value) { var element = vnode.dom - if (key === "key" || (old === value && !isFormAttribute(vnode, key)) || typeof value === "undefined" || isLifecycleMethod(key)) return + if (key === "key" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return var nsLastIndex = key.indexOf(":") if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") { element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value) @@ -402,12 +402,13 @@ module.exports = function($window) { function isAttribute(attr) { return attr === "href" || attr === "list" || attr === "form"// || attr === "type" || attr === "width" || attr === "height" } - function hasIntegrationMethods(vnode) { - return vnode.attrs != null && (vnode.attrs.oncreate || vnode.attrs.onupdate || vnode.attrs.onbeforeremove || vnode.attrs.onremove) + function hasIntegrationMethods(source) { + return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove) } //style function updateStyle(element, old, style) { + if (old === style) element.style = "", old = null if (style == null) element.style = "" else if (typeof style === "string") element.style = style else { diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 4fc8a8b8..c36d27f1 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -537,6 +537,23 @@ o.spec("component", function() { o(called).equals(1) o(root.childNodes.length).equals(0) }) + o("does not recycle when there's an onupdate", function() { + var component = { + onupdate: function() {}, + view: function() { + return {tag: "div"} + } + } + var update = o.spy() + var vnode = {tag: component, key: 1} + var updated = {tag: component, key: 1} + + render(root, [vnode]) + render(root, []) + render(root, [updated]) + + o(vnode.dom).notEquals(updated.dom) + }) }) o.spec("state", function() { o("deep copies state", function() { diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 755108e8..2a260019 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -92,6 +92,24 @@ o.spec("updateElement", function() { o(updated.dom.style.backgroundColor).equals("green") }) + o("handles noop style change when style is string", function() { + var vnode = {tag: "a", attrs: {style: "background-color:green;"}} + var updated = {tag: "a", attrs: {style: "background-color:green;"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("handles noop style change when style is object", function() { + var vnode = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("red") + }) o("updates style from string to object", function() { var vnode = {tag: "a", attrs: {style: "background-color:red;"}} var updated = {tag: "a", attrs: {style: {backgroundColor: "green"}}} @@ -150,6 +168,19 @@ o.spec("updateElement", function() { o(updated.dom.style.backgroundColor).equals("red") o(updated.dom.style.border).equals("") }) + o("updates style when it's same object but mutated", function() { + var style = {backgroundColor: "red", color: "gold"} + var vnode = {tag: "a", attrs: {style: style}} + + render(root, [vnode]) + + delete style.backgroundColor + var updated = {tag: "a", attrs: {style: style}} + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("") + o(updated.dom.style.color).equals("gold") + }) o("replaces el", function() { var vnode = {tag: "a"} var updated = {tag: "b"} diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index 92604018..d980fe2c 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,3 +1,3 @@ "use strict" -module.exports = typeof process === "object" ? process.nextTick : window.setImmediate || window.setTimeout \ No newline at end of file +module.exports = typeof setImmediate === "function" ? setImmediate : setTimeout \ No newline at end of file