From 19d2ba316cdfa7c350aa31251b7e55f501aaeac1 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 1 Oct 2017 16:48:08 -0400 Subject: [PATCH 01/66] Remove support for `vnode.state = ...` - Remove docs for it - Remove tests for it - Add runtime check for unexpected reassignment, since we can't freeze the property (we internally have to be able to modify it) --- docs/change-log.md | 1 + docs/vnodes.md | 3 +- render/render.js | 54 ++++++++++++----- render/tests/test-component.js | 91 ----------------------------- render/tests/test-onbeforeremove.js | 6 +- render/vnode.js | 2 +- 6 files changed, 43 insertions(+), 114 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 722d5975..cc30ad1f 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -18,6 +18,7 @@ - API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) - API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) +- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook. #### News diff --git a/docs/vnodes.md b/docs/vnodes.md index cff0ebe4..783f432f 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -74,7 +74,6 @@ Property | Type | Description `dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range. `domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property). `state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the `state` inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure. -`_state` | `Object?` | For components, a reference to the original `vnode.state` object, used to lookup the `view` and hooks. This property is only used internally by Mithril, do not use or modify it. `events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use or modify it. `instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril, do not use or modify it. `skip` | `Boolean` | This property is only used internally by Mithril when diffing keyed lists, do not use or modify it. @@ -89,7 +88,7 @@ The `tag` property of a vnode determines its type. There are five vnode types: Vnode type | Example | Description ------------ | ------------------------------ | --- Element | `{tag: "div"}` | Represents a DOM element. -Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. +Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. Text | `{tag: "#", children: ""}` | Represents a DOM text node. Trusted HTML | `{tag: "<", children: "
"}` | Represents a list of DOM elements from an HTML string. Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component. diff --git a/render/render.js b/render/render.js index d8de6e0d..51532255 100644 --- a/render/render.js +++ b/render/render.js @@ -18,6 +18,24 @@ module.exports = function($window) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("`vnode.state` must not be modified") + } + + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) + } + } + //create function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { for (var i = start; i < end; i++) { @@ -121,10 +139,9 @@ module.exports = function($window) { sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) } - vnode._state = vnode.state if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - initLifecycle(vnode._state, vnode, hooks) - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") sentinel.$$reentrantLock$$ = null } @@ -240,7 +257,6 @@ module.exports = function($window) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state - vnode._state = old._state vnode.events = old.events if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { @@ -321,10 +337,10 @@ module.exports = function($window) { if (recycling) { initComponent(vnode, hooks) } else { - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - updateLifecycle(vnode._state, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -410,15 +426,16 @@ module.exports = function($window) { } function removeNode(vnode, context) { var expected = 1, called = 0 + var original = vnode.state if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { - var result = vnode._state.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -427,6 +444,7 @@ module.exports = function($window) { continuation() function continuation() { if (++called === expected) { + checkState(vnode, original) onremove(vnode) if (vnode.dom) { var count = vnode.domSize || 1 @@ -450,9 +468,9 @@ module.exports = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) + if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) if (typeof vnode.tag !== "string") { - if (typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) + if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) if (vnode.instance != null) onremove(vnode.instance) } else { var children = vnode.children @@ -611,16 +629,20 @@ module.exports = function($window) { //lifecycle function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) - if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) } function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) } function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old) + } if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize diff --git a/render/tests/test-component.js b/render/tests/test-component.js index c391ddd2..ea3d1624 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -764,97 +764,6 @@ o.spec("component", function() { o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) }) }) - o("lifecycle timing megatest (for a single component with the state overwritten)", function() { - var methods = { - view: o.spy(function(vnode) { - o(vnode.state).equals(1) - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" - ] - hooks.forEach(function(hook) { - // the `attrs` hooks are called before the component ones - attrs[hook] = o.spy(function(vnode) { - o(vnode.state).equals(1) - o(attrs[hook].callCount).equals(methods[hook].callCount + 1) - }) - methods[hook] = o.spy(function(vnode) { - o(vnode.state).equals(1) - o(attrs[hook].callCount).equals(methods[hook].callCount) - }) - }) - - var attrsOninit = attrs.oninit - var methodsOninit = methods.oninit - attrs.oninit = o.spy(function(vnode){ - vnode.state = 1 - return attrsOninit.call(this, vnode) - }) - methods.oninit = o.spy(function(vnode){ - vnode.state = 1 - return methodsOninit.call(this, vnode) - }) - - var component = createComponent(methods) - - o(methods.view.callCount).equals(0) - o(methods.oninit.callCount).equals(0) - o(methods.oncreate.callCount).equals(0) - o(methods.onbeforeupdate.callCount).equals(0) - o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, [{tag: component, attrs: attrs}]) - - o(methods.view.callCount).equals(1) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(0) - o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, [{tag: component, attrs: attrs}]) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, []) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(1) - o(methods.onremove.callCount).equals(1) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - }) o("hook state and arguments validation", function(){ var methods = { view: o.spy(function(vnode) { diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 0e83d4a0..834f7510 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -36,10 +36,7 @@ o.spec("onbeforeremove", function() { o(update.callCount).equals(0) }) o("calls onbeforeremove when removing element", function(done) { - var vnode = {tag: "div", attrs: { - oninit: function() {vnode.state = {}}, - onbeforeremove: remove - }} + var vnode = {tag: "div", attrs: {onbeforeremove: remove}} render(root, [vnode]) render(root, []) @@ -47,6 +44,7 @@ o.spec("onbeforeremove", function() { function remove(node) { o(node).equals(vnode) o(this).equals(vnode.state) + o(this != null && typeof this === "object").equals(true) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) diff --git a/render/vnode.js b/render/vnode.js index ce137703..13ed393f 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) From 77378af23237076d7303742703e68800eb21ec9f Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 4 Oct 2017 22:02:02 +0000 Subject: [PATCH 02/66] Bundled output for commit ae27c0ff183da5c36c9bf0052567d9c2b8ae464c [skip ci] --- README.md | 2 +- mithril.js | 54 ++++++++++++++++++++---------- mithril.min.js | 90 +++++++++++++++++++++++++------------------------- 3 files changed, 83 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 105894ed..21055536 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.43 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.42 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index a953152b..3360f865 100644 --- a/mithril.js +++ b/mithril.js @@ -1,7 +1,7 @@ ;(function() { "use strict" function Vnode(tag, key, attrs0, children, text, dom) { - return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) @@ -398,6 +398,22 @@ var coreRenderer = function($window) { function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("`vnode.state` must not be modified") + } + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) + } + } //create function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { for (var i = start; i < end; i++) { @@ -495,10 +511,9 @@ var coreRenderer = function($window) { sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) } - vnode._state = vnode.state if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - initLifecycle(vnode._state, vnode, hooks) - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") sentinel.$$reentrantLock$$ = null } @@ -612,7 +627,6 @@ var coreRenderer = function($window) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state - vnode._state = old._state vnode.events = old.events if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { @@ -692,10 +706,10 @@ var coreRenderer = function($window) { if (recycling) { initComponent(vnode, hooks) } else { - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - updateLifecycle(vnode._state, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -778,15 +792,16 @@ var coreRenderer = function($window) { } function removeNode(vnode, context) { var expected = 1, called = 0 + var original = vnode.state if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { - var result = vnode._state.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -795,6 +810,7 @@ var coreRenderer = function($window) { continuation() function continuation() { if (++called === expected) { + checkState(vnode, original) onremove(vnode) if (vnode.dom) { var count0 = vnode.domSize || 1 @@ -818,9 +834,9 @@ var coreRenderer = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) + if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) if (typeof vnode.tag !== "string") { - if (typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) + if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) if (vnode.instance != null) onremove(vnode.instance) } else { var children = vnode.children @@ -974,16 +990,20 @@ var coreRenderer = function($window) { } //lifecycle function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) - if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) } function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) } function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old) + } if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize diff --git a/mithril.min.js b/mithril.min.js index 5a8f5c34..cd027993 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,45 +1,45 @@ -(function(){function y(b,d,h,k,q,m){return{tag:b,key:d,attrs:h,children:k,text:q,dom:m,domSize:void 0,state:void 0,_state:void 0,events:void 0,instance:void 0,skip:!1}}function O(b){for(var d in b)if(G.call(b,d))return!1;return!0}function A(b){var d=arguments[1],h=2;if(null==b||"string"!==typeof b&&"function"!==typeof b&&"function"!==typeof b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b){var k;if(!(k=P[b])){var q="div";for(var m=[],l={};k=R.exec(b);){var r= -k[1],f=k[2];""===r&&""!==f?q=f:"#"===r?l.id=f:"."===r?m.push(f):"["===k[3][0]&&((r=k[6])&&(r=r.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===k[4]?m.push(r):l[k[4]]=""===r?r:r||!0)}0a.indexOf("?")?"?":"&";a+=f+d}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(v){throw Error(a);}}function r(a){return a.responseText}function f(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dn.status||304===n.status||U.test(a.url))d(f(a.type,b));else{var k=Error(n.responseText);k.code=n.status;k.response=b;h(k)}}catch(g){h(g)}};k&&null!=a.data?n.send(a.data):n.send()});return!0===a.background?v:p(v)},jsonp:function(a,l){var r=h();a=k(a,l);var p=new d(function(d,h){var k=a.callbackName||"_mithril_"+ -Math.round(1E16*Math.random())+"_"+n++,l=b.document.createElement("script");b[k]=function(h){l.parentNode.removeChild(l);d(f(a.type,h));delete b[k]};l.onerror=function(){l.parentNode.removeChild(l);h(Error("JSONP request failed"));delete b[k]};null==a.data&&(a.data={});a.url=q(a.url,a.data);a.data[a.callbackKey||"callback"]=k;l.src=m(a.url,a.data);b.document.documentElement.appendChild(l)});return!0===a.background?p:r(p)},setCompletionCallback:function(a){p=a}}}(window,w),Q=function(b){function d(g, -c,e,a,b,d,k){for(;e=x&&F>=p;){var z=c[x],t=e[p];if(z!==t||b)if(null==z)x++;else if(null==t)p++;else if(z.key===t.key){var B=null!= -C&&x>=c.length-C.length||null==C&&b;x++;p++;l(g,z,t,k,f(c,x,m),B,q);b&&z.tag===t.tag&&n(g,r(z),m)}else if(z=c[v],z!==t||b)if(null==z)v--;else if(null==t)p++;else if(z.key===t.key)B=null!=C&&v>=c.length-C.length||null==C&&b,l(g,z,t,k,f(c,v+1,m),B,q),(b||p=x&&F>=p;){z=c[v];t=e[F];if(z!==t||b)if(null==z)v--;else{if(null!=t)if(z.key===t.key)B=null!=C&&v>=c.length-C.length||null==C&&b,l(g,z,t,k,f(c,v+1,m),B,q),b&&z.tag===t.tag&& -n(g,r(z),m),null!=z.dom&&(m=z.dom),v--;else{if(!u){u=c;z=v;B={};var w;for(w=0;wb.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function q(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||V.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(W){e(W)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:t(v)},jsonp:function(b,g){var t=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ +Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,r),R=function(a){function c(h, +d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&x>=p;){var y= +d[u],z=a[p];if(y!==z||c)if(null==y)u++;else if(null==z)p++;else if(y.key===z.key){var r=null!=q&&u>=d.length-q.length||null==q&&c;u++;p++;m(h,y,z,e,t(d,u,g),r,k);c&&y.tag===z.tag&&b(h,l(y),g)}else if(y=d[v],y!==z||c)if(null==y)v--;else if(null==z)p++;else if(y.key===z.key)r=null!=q&&v>=d.length-q.length||null==q&&c,m(h,y,z,e,t(d,v+1,g),r,k),(c||p=u&&x>=p;){y=d[v];z=a[x];if(y!==z||c)if(null==y)v--;else{if(null!=z)if(y.key=== +z.key)r=null!=q&&v>=d.length-q.length||null==q&&c,m(h,y,z,e,t(d,v+1,g),r,k),c&&y.tag===z.tag&&b(h,l(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!I){I=d;y=v;r={};var w;for(w=0;w Date: Tue, 10 Oct 2017 22:53:19 -0400 Subject: [PATCH 03/66] Handle newlines in error messages, fixes #1495 --- docs/change-log.md | 1 + ospec/ospec.js | 5 ++++- ospec/tests/test-ospec.js | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index c365276d..9dc3e7b4 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -29,6 +29,7 @@ - Added support for async functions and promises in tests - ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) - Error handling for async tests with `done` callbacks supports error as first argument +- Error messages which include newline characters do not swallow the stack trace ([#1495](https://github.com/MithrilJS/mithril.js/issues/1495)) #### Bug fixes diff --git a/ospec/ospec.js b/ospec/ospec.js index 147e2dc4..5c9b223a 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -49,6 +49,9 @@ module.exports = new function init(name) { spy.callCount = 0 return spy } + o.cleanStackTrace = function(stack) { + return stack.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/gm).pop() + } o.run = function() { results = [] start = new Date @@ -235,7 +238,7 @@ module.exports = new function init(name) { var status = 0 for (var i = 0, r; r = results[i]; i++) { if (!r.pass) { - var stackTrace = r.error.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + var stackTrace = o.cleanStackTrace(r.error) console.error(r.context + ":\n" + highlight(r.message) + (stackTrace ? "\n\n" + stackTrace + "\n\n" : ""), hasProcess ? "" : "color:red", hasProcess ? "" : "color:black") status = 1 } diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 69f2e1f1..9b9f24d5 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -149,6 +149,18 @@ o.spec("ospec", function() { }) }) + o.spec('stack trace cleaner', function() { + o('handles line breaks', function() { + try { + throw new Error('line\nbreak') + } catch(error) { + var trace = o.cleanStackTrace(error.stack) + o(trace).notEquals('break') + o(trace.includes("test-ospec.js")).equals(true) + } + }) + }) + o.spec("async promise", function() { var a = 0, b = 0 From 8b56c7091169c0fb057d7afac041a23a4700bcdb Mon Sep 17 00:00:00 2001 From: Mateusz Jaworski Date: Sat, 14 Oct 2017 07:31:03 +0200 Subject: [PATCH 04/66] fix: Allow for changing focus in lifecycle hooks (#1988) --- docs/change-log.md | 1 + render/render.js | 2 +- render/tests/test-input.js | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index c365276d..99997ba7 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -40,6 +40,7 @@ - core: `Object.prototype` properties can no longer interfere with event listener calls. - API: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed. - core: `xlink:href` attributes are now correctly removed +- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks --- diff --git a/render/render.js b/render/render.js index 0b881b2b..8674925b 100644 --- a/render/render.js +++ b/render/render.js @@ -664,9 +664,9 @@ module.exports = function($window) { if (!Array.isArray(vnodes)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) dom.vnodes = vnodes - for (var i = 0; i < hooks.length; i++) hooks[i]() // document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement if (active != null && $doc.activeElement !== active) active.focus() + for (var i = 0; i < hooks.length; i++) hooks[i]() } return {render: render, setEventCallback: setEventCallback} diff --git a/render/tests/test-input.js b/render/tests/test-input.js index d61bad54..1f9a24d7 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -30,6 +30,16 @@ o.spec("form inputs", function() { o($window.document.activeElement).equals(input.dom) }) + o("maintains focus when changed manually in hook", function() { + var input = {tag: "input", attrs: {oncreate: function() { + input.dom.focus(); + }}}; + + render(root, [input]) + + o($window.document.activeElement).equals(input.dom) + }) + o("syncs input value if DOM value differs from vdom value", function() { var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}} var updated = {tag: "input", attrs: {value: "aaa", oninput: function() {}}} From 1e41f0c9a33e73c46628a8fbbc400eda3cd7cf1c Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sat, 14 Oct 2017 05:32:56 +0000 Subject: [PATCH 05/66] Bundled output for commit 8b56c7091169c0fb057d7afac041a23a4700bcdb [skip ci] --- mithril.js | 2 +- mithril.min.js | 54 +++++++++++++++++++++++------------------------ package-lock.json | 18 ++++++++-------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mithril.js b/mithril.js index 3360f865..4aa0ae29 100644 --- a/mithril.js +++ b/mithril.js @@ -1022,9 +1022,9 @@ var coreRenderer = function($window) { if (!Array.isArray(vnodes)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) dom.vnodes = vnodes - for (var i = 0; i < hooks.length; i++) hooks[i]() // document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement if (active != null && $doc.activeElement !== active) active.focus() + for (var i = 0; i < hooks.length; i++) hooks[i]() } return {render: render, setEventCallback: setEventCallback} } diff --git a/mithril.min.js b/mithril.min.js index cd027993..18f6fe49 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,45 +1,45 @@ -(function(){function w(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function O(a){for(var c in a)if(D.call(a,c))return!1;return!0}function A(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=P[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var q=f[1], +(function(){function w(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function O(a){for(var c in a)if(E.call(a,c))return!1;return!0}function z(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=P[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var q=f[1], m=f[2];""===q&&""!==m?n=m:"#"===q?g.id=m:"."===q?k.push(m):"["===f[3][0]&&((q=f[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(q):g[f[4]]=""===q?q:q||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function q(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||V.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(W){e(W)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:t(v)},jsonp:function(b,g){var t=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,r),R=function(a){function c(h, -d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&x>=p;){var y= -d[u],z=a[p];if(y!==z||c)if(null==y)u++;else if(null==z)p++;else if(y.key===z.key){var r=null!=q&&u>=d.length-q.length||null==q&&c;u++;p++;m(h,y,z,e,t(d,u,g),r,k);c&&y.tag===z.tag&&b(h,l(y),g)}else if(y=d[v],y!==z||c)if(null==y)v--;else if(null==z)p++;else if(y.key===z.key)r=null!=q&&v>=d.length-q.length||null==q&&c,m(h,y,z,e,t(d,v+1,g),r,k),(c||p=u&&x>=p;){y=d[v];z=a[x];if(y!==z||c)if(null==y)v--;else{if(null!=z)if(y.key=== -z.key)r=null!=q&&v>=d.length-q.length||null==q&&c,m(h,y,z,e,t(d,v+1,g),r,k),c&&y.tag===z.tag&&b(h,l(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!I){I=d;y=v;r={};var w;for(w=0;w=u&&D>=p;){var x= +d[u],r=a[p];if(x!==r||c)if(null==x)u++;else if(null==r)p++;else if(x.key===r.key){var C=null!=q&&u>=d.length-q.length||null==q&&c;u++;p++;m(h,x,r,e,t(d,u,g),C,k);c&&x.tag===r.tag&&b(h,l(x),g)}else if(x=d[v],x!==r||c)if(null==x)v--;else if(null==r)p++;else if(x.key===r.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,x,r,e,t(d,v+1,g),C,k),(c||p=u&&D>=p;){x=d[v];r=a[D];if(x!==r||c)if(null==x)v--;else{if(null!=r)if(x.key=== +r.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,x,r,e,t(d,v+1,g),C,k),c&&x.tag===r.tag&&b(h,l(x),g),null!=x.dom&&(g=x.dom),v--;else{if(!I){I=d;x=v;C={};var w;for(w=0;w Date: Sat, 14 Oct 2017 06:09:46 +0000 Subject: [PATCH 06/66] docs: fix v1.1.4 link --- docs/change-log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 99997ba7..214a98a6 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,7 +1,7 @@ # Change log - [v2.0.0](#v200-wip) -- [v1.1.4](#v113) +- [v1.1.4](#v114) - [v1.1.3](#v113) - [v1.1.2](#v112) - [v1.1.1](#v111) From 0691662fd4ffd77dd3cd8ee0280286dd26edb47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Sat, 14 Oct 2017 08:50:15 +0200 Subject: [PATCH 07/66] change log: add v1.1.5 in next --- docs/change-log.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 214a98a6..6c5adc35 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -33,7 +33,6 @@ #### Bug fixes - API: `m.route.set()` causes all mount points to be redrawn ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) -- API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924)) - API: Using style objects in hyperscript calls will now properly diff style properties from one render to another as opposed to re-writing all element style properties every render. - core: `addEventListener` and `removeEventListener` are always used to manage event subscriptions, preventing external interference. - core: Event listeners allocate less memory, swap at low cost, and are properly diffed now when rendered via `m.mount()`/`m.redraw()`. @@ -44,6 +43,12 @@ --- +### v1.1.5 + +- API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924)) + +--- + ### v1.1.4 #### Bug fixes: From 622e00981138601d8e9a309013e0f251a0e3d44b Mon Sep 17 00:00:00 2001 From: valtron Date: Mon, 16 Oct 2017 00:38:44 -0600 Subject: [PATCH 08/66] recycling => shouldRecycle, Fix #1992 (#1993) * Fix #1992 * doc in changelog * add test for #1992 --- docs/change-log.md | 1 + render/render.js | 2 +- render/tests/test-oninit.js | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 6c5adc35..a2f8259d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -40,6 +40,7 @@ - API: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed. - core: `xlink:href` attributes are now correctly removed - core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks +- render: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992) --- diff --git a/render/render.js b/render/render.js index 8674925b..37855447 100644 --- a/render/render.js +++ b/render/render.js @@ -235,7 +235,7 @@ module.exports = function($window) { if (oldIndex != null) { var movable = old[oldIndex] var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true if (movable.dom != null) nextSibling = movable.dom diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index 4d94cae4..f6ffb873 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -199,4 +199,27 @@ o.spec("oninit", function() { o(vnode.dom.oninit).equals(undefined) o(vnode.dom.attributes["oninit"]).equals(undefined) }) + + o("No spurious oninit calls in mapped keyed diff when the pool is involved (#1992)", function () { + var oninit1 = o.spy() + var oninit2 = o.spy() + var oninit3 = o.spy() + + render(root, [ + {tag: "p", key: 1, attrs: {oninit: oninit1}}, + {tag: "p", key: 2, attrs: {oninit: oninit2}}, + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + render(root, [ + {tag: "p", key: 1, attrs: {oninit: oninit1}}, + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + render(root, [ + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + + o(oninit1.callCount).equals(1) + o(oninit2.callCount).equals(1) + o(oninit3.callCount).equals(1) + }) }) From f03427db8565bd93c8a491ad6aafbbc6788983e2 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 16 Oct 2017 06:40:15 +0000 Subject: [PATCH 09/66] Bundled output for commit 622e00981138601d8e9a309013e0f251a0e3d44b [skip ci] --- mithril.js | 2 +- mithril.min.js | 78 +++++++++++++++++++++++++------------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/mithril.js b/mithril.js index 4aa0ae29..91e870e5 100644 --- a/mithril.js +++ b/mithril.js @@ -605,7 +605,7 @@ var coreRenderer = function($window) { if (oldIndex != null) { var movable = old[oldIndex] var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true if (movable.dom != null) nextSibling = movable.dom diff --git a/mithril.min.js b/mithril.min.js index 18f6fe49..fbaa5741 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,45 +1,45 @@ -(function(){function w(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function O(a){for(var c in a)if(E.call(a,c))return!1;return!0}function z(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=P[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var q=f[1], -m=f[2];""===q&&""!==m?n=m:"#"===q?g.id=m:"."===q?k.push(m):"["===f[3][0]&&((q=f[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(q):g[f[4]]=""===q?q:q||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function q(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cb.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(w){throw Error(b);}}function p(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||V.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(W){e(W)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:t(v)},jsonp:function(b,g){var t=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ -Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,r),R=function(a){function c(h, +"function"===typeof b.config&&(l=b.config(l,b)||l);l.onreadystatechange=function(){if(!t&&4===l.readyState)try{var a=b.extract!==p?b.extract(l,b):b.deserialize(b.extract(l,b));if(200<=l.status&&300>l.status||304===l.status||V.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(W){e(W)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?w:t(w)},jsonp:function(b,g){var t=e();b=f(b,g);var p=new c(function(c,e){var f=b.callbackName||"_mithril_"+ +Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?p:t(p)},setCompletionCallback:function(b){t=b}}}(window,r),R=function(a){function c(h, d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&D>=p;){var x= -d[u],r=a[p];if(x!==r||c)if(null==x)u++;else if(null==r)p++;else if(x.key===r.key){var C=null!=q&&u>=d.length-q.length||null==q&&c;u++;p++;m(h,x,r,e,t(d,u,g),C,k);c&&x.tag===r.tag&&b(h,l(x),g)}else if(x=d[v],x!==r||c)if(null==x)v--;else if(null==r)p++;else if(x.key===r.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,x,r,e,t(d,v+1,g),C,k),(c||p=u&&D>=p;){x=d[v];r=a[D];if(x!==r||c)if(null==x)v--;else{if(null!=r)if(x.key=== -r.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,x,r,e,t(d,v+1,g),C,k),c&&x.tag===r.tag&&b(h,l(x),g),null!=x.dom&&(g=x.dom),v--;else{if(!I){I=d;x=v;C={};var w;for(w=0;w=u&&C>=q;){var x= +d[u],r=a[q];if(x!==r||c)if(null==x)u++;else if(null==r)q++;else if(x.key===r.key){var D=null!=p&&u>=d.length-p.length||null==p&&c;u++;q++;m(h,x,r,e,t(d,u,g),D,k);c&&x.tag===r.tag&&b(h,l(x),g)}else if(x=d[w],x!==r||c)if(null==x)w--;else if(null==r)q++;else if(x.key===r.key)D=null!=p&&w>=d.length-p.length||null==p&&c,m(h,x,r,e,t(d,w+1,g),D,k),(c||q=u&&C>=q;){x=d[w];r=a[C];if(x!==r||c)if(null==x)w--;else{if(null!=r)if(x.key=== +r.key)D=null!=p&&w>=d.length-p.length||null==p&&c,m(h,x,r,e,t(d,w+1,g),D,k),c&&x.tag===r.tag&&b(h,l(x),g),null!=x.dom&&(g=x.dom),w--;else{if(!I){I=d;D=w;x={};var v;for(v=0;v=d.length-p.length||null==p&&c,m(h,v,r,e,t(d,w+1,g),D,k),b(h,l(v),g),d[x].skip=!0,null!=v.dom&&(g=v.dom)):g=n(h,r,e,k,g))}C--}else w--,C--;if(C Date: Mon, 16 Oct 2017 08:56:24 -0700 Subject: [PATCH 10/66] docs: add v1.1.5 TOC entry --- docs/change-log.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/change-log.md b/docs/change-log.md index a2f8259d..8fb0ddf2 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,7 @@ # Change log - [v2.0.0](#v200-wip) +- [v1.1.5](#v115) - [v1.1.4](#v114) - [v1.1.3](#v113) - [v1.1.2](#v112) From 5400501aa7d650d39c0e06fa219c4a1e0f296cb9 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 17 Oct 2017 05:36:31 +0000 Subject: [PATCH 11/66] docs: reformat & flesh out webpack quickstart Fixes #1995 --- docs/installation.md | 58 +++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index c4604ad5..57efabe6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -17,28 +17,48 @@ If you're new to Javascript or just want a very simple setup to get your feet we #### Quick start with Webpack + +1. Initialize the directory as an npm package ```bash -# 1) install -npm install mithril --save - -npm install webpack --save - -# 2) add this line into the scripts section in package.json -# "scripts": { -# "start": "webpack src/index.js bin/app.js --watch" -# } - -# 3) create an `src/index.js` file - -# 4) create an `index.html` file containing `` - -# 5) run bundler -npm start - -# 6) open `index.html` in the (default) browser -open index.html +$> npm init --yes ``` +2. install required tools +```bash +$> npm install mithril --save +$> npm install webpack --save +``` + +3. Add a "start" entry to the scripts section in `package.json` +```js +{ + // ... + "scripts": { + "start": "webpack src/index.js bin/app.js --watch" + } +} +``` + +3. Create `src/index.js` +```js +import m from "mithril"; + +m.render(document.body, "hello world"); +``` + +4. create `index.html` +```html + + + +``` + +5. run bundler +```bash +$> npm start +``` +6. open `index.html` in your (default) browser + #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. From 1a6b6a591c76ec6d4918840331188eb5ee0f0785 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 17 Oct 2017 15:25:15 -0700 Subject: [PATCH 12/66] chore(ospec): merge package.json @ 1.4.0 back --- ospec/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ospec/package.json b/ospec/package.json index 7cfa4595..acc0ae47 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -1,6 +1,6 @@ { "name": "ospec", - "version": "1.3.0", + "version": "1.4.0", "description": "Noiseless testing framework", "main": "ospec.js", "directories": { From bd83b32709453ea78b01585078f04e886100d8e4 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Tue, 17 Oct 2017 15:37:43 -0700 Subject: [PATCH 13/66] docs: Add ospec releasing process & TOC --- docs/releasing.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/releasing.md b/docs/releasing.md index 37bec28d..ce4a871d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -2,6 +2,10 @@ **Note** These steps all assume that `MithrilJS/mithril.js` is a git remote named `mithriljs`, adjust accordingly if that doesn't match your setup. +- [Releasing a new Mithril version](#releasing-a-new-mithril-version) +- [Updating mithril.js.org](#updating-mithriljsorg) +- [Releasing a new ospec version](#releasing-a-new-ospec-version) + ## Releasing a new Mithril version ### Prepare the release @@ -112,3 +116,61 @@ $ git push mithriljs ``` After the Travis build completes the updated docs should appear on https://mithril.js.org in a few minutes. + +## Releasing a new ospec version + +1. Ensure your local branch is up to date + +```bash +$ git co next +$ git pull --rebase mithriljs next +``` + +2. Determine patch level of the change +3. Update `version` field in `ospec/package.json` to match new version being prepared for release +4. Commit changes to `next` + +``` +$ git add . +$ git commit -m "chore(ospec): ospec@" + +# Push to your branch +$ git push + +# Push to MithrilJS/mithril.js +$ git push mithriljs next +``` + +### Merge from `next` to `master` + +5. Switch to `master` and make sure it's up to date + +```bash +$ git co master +$ git pull --rebase mithriljs master +``` + +6. merge `next` on top of it + +```bash +$ git checkout next -- ./ospec +$ git add . +$ git commit -m "chore(ospec): ospec@" +``` + +7. Ensure the tests are passing! + +### Publish the release + +8. Push the changes to `MithrilJS/mithril.js` + +```bash +$ git push mithriljs master +``` + +9. Publish the changes to npm **from the `/ospec` folder**. That bit is important to ensure you don't accidentally ship a new Mithril release! + +```bash +$ cd ./ospec +$ npm publish +``` From f88da1c6da88f1cb221165b4156ceeae295d688c Mon Sep 17 00:00:00 2001 From: Sage Gerard Date: Fri, 27 Oct 2017 15:25:34 -0400 Subject: [PATCH 14/66] Clarify source of 2nd render pass re: preloading data While the docs do say that a second render pass for preloaded data comes from request completion, the example code for preloading data suggests that a promise chain returned from `oninit` has a role to play in controlling the second render pass. The docs should make explicit where the redraw is initiated so the reader does not mistakingly believe that `oninit()` retuning a promise changes anything. --- docs/route.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/route.md b/docs/route.md index 73b0e395..faa5623b 100644 --- a/docs/route.md +++ b/docs/route.md @@ -588,7 +588,7 @@ m.route(document.body, "/secret", { #### Preloading data -Typically, a component can load data upon initialization. Loading data this way renders the component twice (once upon routing, and once after the request completes). +Typically, a component can load data upon initialization. Loading data this way renders the component twice. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that `loadUsers()` returns a Promise, but any Promise returned by `oninit` is currently ignored. The second render pass comes from the [`background` option for `m.request`](request.md). ```javascript var state = { From 0736f848cb19778b980fe93ef428c4cc2957efde Mon Sep 17 00:00:00 2001 From: spacejack Date: Sat, 28 Oct 2017 17:39:32 -0400 Subject: [PATCH 15/66] Silence global-require rule in ospec, add ecma 2017 parser to eslintrc for async/await in test-ospec --- .eslintrc.js | 5 ++++- ospec/ospec.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c14d0119..4eeef493 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,10 @@ module.exports = { "commonjs": true, "es6": true, "node": true - }, + }, + "parserOptions": { + "ecmaVersion": 2017, + }, "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", diff --git a/ospec/ospec.js b/ospec/ospec.js index 147e2dc4..fabf4de8 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-bitwise, no-process-exit */ +/* eslint-disable global-require, no-bitwise, no-process-exit */ "use strict" module.exports = new function init(name) { From 3fe56bb503edbac9eb8dbb6e7e5a80f4491a068f Mon Sep 17 00:00:00 2001 From: spacejack Date: Sat, 28 Oct 2017 18:32:58 -0400 Subject: [PATCH 16/66] Revert eslintrc change, return Promise instead of using async/await --- .eslintrc.js | 3 --- ospec/tests/test-ospec.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4eeef493..a38c5e1b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,9 +5,6 @@ module.exports = { "es6": true, "node": true }, - "parserOptions": { - "ecmaVersion": 2017, - }, "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 69f2e1f1..d1ddf496 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -188,8 +188,8 @@ o.spec("ospec", function() { }) }) - o("promise functions", async function() { - await wrapPromise(function() { + o("promise functions", function() { + return wrapPromise(function() { o(a).equals(b) o(a).equals(1)("a and b should be initialized") }) From efb0e94d99b78533475859bad3c1c84238fa17d5 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Sat, 28 Oct 2017 22:57:52 -0700 Subject: [PATCH 17/66] fix: closure compiler requires HTTPS now (#2007) Also barfing out the response from the server when JSON parsing errors happen --- bundler/minify.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bundler/minify.js b/bundler/minify.js index 51b2b775..3b1d3cae 100644 --- a/bundler/minify.js +++ b/bundler/minify.js @@ -1,6 +1,6 @@ "use strict" -var http = require("http") +var http = require("https") var querystring = require("querystring") var fs = require("fs") @@ -22,7 +22,6 @@ module.exports = function(input, output, options, done) { var response = "" var req = http.request({ method: "POST", - protocol: "http:", hostname: "closure-compiler.appspot.com", path: "/compile", headers: { @@ -33,8 +32,16 @@ module.exports = function(input, output, options, done) { res.on("data", function(chunk) { response += chunk.toString() }) + res.on("end", function() { - var results = JSON.parse(response) + try { + var results = JSON.parse(response) + } catch(e) { + console.error(response); + + throw e; + } + if (results.errors) { for (var i = 0; i < results.errors.length; i++) console.log(results.errors[i]) } From 217b9c194cea23c0dc2ed83ebf2c33aaba67d2a7 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Sat, 28 Oct 2017 22:58:21 -0700 Subject: [PATCH 18/66] chore: update CODEOWNERS to match reality --- .github/CODEOWNERS | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ab8484ef..6485e4d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,3 @@ -# Leo owns everything, for better or worse -* @lhorie - .travis.yml @tivac package.json @tivac .npmignore @tivac @@ -9,4 +6,4 @@ package.json @tivac README.md @tivac docs/ @tivac performance/ @tivac -render/ @isiahmeadows +render/ @isiahmeadows @pygy From 131e61002eef422ebe20a61a98de376e22b5817c Mon Sep 17 00:00:00 2001 From: Patrik Johnson Date: Tue, 8 Aug 2017 15:06:30 +0300 Subject: [PATCH 19/66] Enable setting navigation options with m.route.link API --- api/router.js | 8 ++++++-- api/tests/test-router.js | 30 ++++++++++++++++++++++++++++++ docs/change-log.md | 1 + docs/route.md | 20 +++++++++++++------- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/api/router.js b/api/router.js index 40ccb7cd..4d893acc 100644 --- a/api/router.js +++ b/api/router.js @@ -52,7 +52,7 @@ module.exports = function($window, redrawService) { } route.get = function() {return currentPath} route.prefix = function(prefix) {routeService.prefix = prefix} - route.link = function(vnode) { + var link = function(options, vnode) { vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href) vnode.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return @@ -60,9 +60,13 @@ module.exports = function($window, redrawService) { e.redraw = false var href = this.getAttribute("href") if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) - route.set(href, undefined, undefined) + route.set(href, undefined, options) } } + route.link = function(args) { + if (args.tag == null) return link.bind(link, args) + return link({}, args) + } route.param = function(key) { if(typeof attrs !== "undefined" && typeof key !== "undefined") return attrs[key] return attrs diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 69aff4ca..f5336625 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -281,6 +281,36 @@ o.spec("route", function() { o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") }) + o("passes options on route.link", function() { + var opts = {} + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + $window.location.href = prefix + "/" + + route(root, "/", { + "/" : { + view: function() { + return m("a", { + href: "/test", + oncreate: route.link(opts) + }) + } + }, + "/test" : { + view : function() { + return m("div") + } + } + }) + route.set = o.spy(route.set) + + root.firstChild.dispatchEvent(e) + + o(route.set.callCount).equals(1) + o(route.set.args[2]).equals(opts) + }) + o("accepts RouteResolver with onmatch that returns Component", function(done) { var matchCount = 0 var renderCount = 0 diff --git a/docs/change-log.md b/docs/change-log.md index be3b48be..09b28302 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -25,6 +25,7 @@ - API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) - API: Event handlers may also be objects with `handleEvent` methods ([#1939](https://github.com/MithrilJS/mithril.js/issues/1939)). +- API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930)) #### Ospec improvements: diff --git a/docs/route.md b/docs/route.md index faa5623b..712c6845 100644 --- a/docs/route.md +++ b/docs/route.md @@ -104,7 +104,7 @@ Argument | Type | Required | Description This function can be used as the `oncreate` (and `onupdate`) hook in a `m("a")` vnode: ```JS -m("a[href=/]", {oncreate: m.route.link})`. +m("a[href=/]", {oncreate: m.route.link}) ``` Using `m.route.link` as a `oncreate` hook causes the link to behave as a router link (i.e. it navigates to the route specified in `href`, instead of navigating away from the current page to the URL specified in `href`. @@ -112,15 +112,21 @@ Using `m.route.link` as a `oncreate` hook causes the link to behave as a router If the `href` attribute is not static, the `onupdate` hook must also be set: ```JS -m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link})` +m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link}) ``` -`m.route.link(vnode)` +`m.route.link` can also set the `options` passed to `m.route.set` when the link is clicked by calling the function in the lifecycle methods: -Argument | Type | Required | Description ------------------ | ----------- | -------- | --- -`vnode` | `Vnode` | Yes | This method is meant to be used as or in conjunction with an `` [vnode](vnodes.md)'s [`oncreate` and `onupdate` hooks](lifecycle-methods.md) -**returns** | | | Returns `undefined` +```JS +m("a[href=/]", {oncreate: m.route.link({replace: true})}) +``` + +`m.route.link(args)` + +Argument | Type | Required | Description +----------------- | ---------------| -------- | --- +`args` | `Vnode|Object` | Yes | This method is meant to be used as or in conjunction with an `` [vnode](vnodes.md)'s [`oncreate` and `onupdate` hooks](lifecycle-methods.md) +**returns** | `function` | | Returns the onclick handler function for the component ##### m.route.param From f22d7d4ca9e8d882d097e911d8e7921f9c128914 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sun, 29 Oct 2017 10:35:41 +0000 Subject: [PATCH 20/66] Bundled output for commit 1a81c1cfebed69193d7c5a4f7951bfc6a39a5533 [skip ci] --- README.md | 2 +- mithril.js | 8 +++-- mithril.min.js | 91 +++++++++++++++++++++++++------------------------- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 21055536..d931eeb5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.42 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.44 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index 91e870e5..3d5c1d1b 100644 --- a/mithril.js +++ b/mithril.js @@ -1264,7 +1264,7 @@ var _20 = function($window, redrawService0) { } route.get = function() {return currentPath} route.prefix = function(prefix0) {routeService.prefix = prefix0} - route.link = function(vnode1) { + var link = function(options, vnode1) { vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href) vnode1.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return @@ -1272,9 +1272,13 @@ var _20 = function($window, redrawService0) { e.redraw = false var href = this.getAttribute("href") if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) - route.set(href, undefined, undefined) + route.set(href, undefined, options) } } + route.link = function(args0) { + if (args0.tag == null) return link.bind(link, args0) + return link({}, args0) + } route.param = function(key3) { if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3] return attrs3 diff --git a/mithril.min.js b/mithril.min.js index fbaa5741..f9b5258f 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,45 +1,46 @@ -(function(){function v(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function O(a){for(var c in a)if(E.call(a,c))return!1;return!0}function z(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=P[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var p=f[1], -m=f[2];""===p&&""!==m?n=m:"#"===p?g.id=m:"."===p?k.push(m):"["===f[3][0]&&((p=f[6])&&(p=p.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(p):g[f[4]]=""===p?p:p||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(w){throw Error(b);}}function p(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||V.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(W){e(W)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?w:t(w)},jsonp:function(b,g){var t=e();b=f(b,g);var p=new c(function(c,e){var f=b.callbackName||"_mithril_"+ -Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?p:t(p)},setCompletionCallback:function(b){t=b}}}(window,r),R=function(a){function c(h, -d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&C>=q;){var x= -d[u],r=a[q];if(x!==r||c)if(null==x)u++;else if(null==r)q++;else if(x.key===r.key){var D=null!=p&&u>=d.length-p.length||null==p&&c;u++;q++;m(h,x,r,e,t(d,u,g),D,k);c&&x.tag===r.tag&&b(h,l(x),g)}else if(x=d[w],x!==r||c)if(null==x)w--;else if(null==r)q++;else if(x.key===r.key)D=null!=p&&w>=d.length-p.length||null==p&&c,m(h,x,r,e,t(d,w+1,g),D,k),(c||q=u&&C>=q;){x=d[w];r=a[C];if(x!==r||c)if(null==x)w--;else{if(null!=r)if(x.key=== -r.key)D=null!=p&&w>=d.length-p.length||null==p&&c,m(h,x,r,e,t(d,w+1,g),D,k),c&&x.tag===r.tag&&b(h,l(x),g),null!=x.dom&&(g=x.dom),w--;else{if(!I){I=d;D=w;x={};var v;for(v=0;v=d.length-p.length||null==p&&c,m(h,v,r,e,t(d,w+1,g),D,k),b(h,l(v),g),d[x].skip=!0,null!=v.dom&&(g=v.dom)):g=n(h,r,e,k,g))}C--}else w--,C--;if(Cb.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function q(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||W.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ +Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:y(q)},setCompletionCallback:function(b){z=b}}}(window,p),R=function(a){function c(h, +d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=t&&x>=r;){var w= +d[t],p=a[r];if(w!==p||c)if(null==w)t++;else if(null==p)r++;else if(w.key===p.key){var C=null!=q&&t>=d.length-q.length||null==q&&c;t++;r++;m(h,w,p,e,z(d,t,g),C,k);c&&w.tag===p.tag&&b(h,l(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)r++;else if(w.key===p.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),C,k),(c||r=t&&x>=r;){w=d[v];p=a[x];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key=== +p.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),C,k),c&&w.tag===p.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;C=v;w={};var u;for(u=0;u=d.length-q.length||null==q&&c,m(h,u,p,e,z(d,v+1,g),C,k),b(h,l(u),g),d[w].skip=!0,null!=u.dom&&(g=u.dom)):g=n(h,p,e,k,g))}x--}else v--,x--;if(x Date: Wed, 1 Nov 2017 03:37:51 +0800 Subject: [PATCH 21/66] Trying to fix #1916 (#1918) * Trying to fix #1916 * Remove test for rendering select options. Add back after resolving issue #1978. * Add #1916 fix to change log. * Revert "Remove test for rendering select options. Add back after resolving issue #1978." This reverts commit d4c1be7c2319adf744f78ca787485f52be869208. * Comment on why failing test for #1916 is commented out. --- docs/change-log.md | 1 + render/render.js | 2 +- render/tests/test-attributes.js | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 09b28302..087202a9 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -44,6 +44,7 @@ - core: `xlink:href` attributes are now correctly removed - core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks - render: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992) +- render: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) --- diff --git a/render/render.js b/render/render.js index 37855447..4fabfc45 100644 --- a/render/render.js +++ b/render/render.js @@ -551,7 +551,7 @@ module.exports = function($window) { } } function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.dom.parentNode === $doc.activeElement } function isLifecycleMethod(attr) { return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index d54ed9d4..62cb4196 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -462,6 +462,19 @@ o.spec("attributes", function() { {tag:"option", attrs: {value: ""}} ]} } + /* FIXME + This incomplete test is meant for testing #1916. + However it cannot be completed until #1978 is addressed + which is a lack a working select.selected / option.selected + attribute. Ask isiahmeadows. + + o("render select options", function() { + var select = {tag: "select", selectedIndex: 0, children: [ + {tag:"option", attrs: {value: "1", selected: ""}} + ]} + render(root, select) + }) + */ o("can be set as text", function() { var a = makeSelect() var b = makeSelect("2") From 021b11eff070bfcdf97b8f6eb061beb407b59dc7 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Tue, 31 Oct 2017 19:43:44 +0000 Subject: [PATCH 22/66] Bundled output for commit db2a12dec97d5b42e353582d3ee09f701d27eb1e [skip ci] --- README.md | 2 +- mithril.js | 2 +- mithril.min.js | 62 +++++++++++++++++++++++++------------------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index d931eeb5..214d677a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.44 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.45 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index 3d5c1d1b..57faabb4 100644 --- a/mithril.js +++ b/mithril.js @@ -916,7 +916,7 @@ var coreRenderer = function($window) { } } function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.dom.parentNode === $doc.activeElement } function isLifecycleMethod(attr) { return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" diff --git a/mithril.min.js b/mithril.min.js index f9b5258f..417ab72b 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,8 +1,8 @@ -(function(){function u(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function N(a){for(var c in a)if(D.call(a,c))return!1;return!0}function B(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=O[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var q=f[1], -m=f[2];""===q&&""!==m?n=m:"#"===q?g.id=m:"."===q?k.push(m):"["===f[3][0]&&((q=f[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(q):g[f[4]]=""===q?q:q||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try typeof b.user?b.user:void 0,"string"===typeof b.password?b.password:void 0);b.serialize!==JSON.stringify||!f||b.headers&&b.headers.hasOwnProperty("Content-Type")||l.setRequestHeader("Content-Type","application/json; charset=utf-8");b.deserialize!==g||b.headers&&b.headers.hasOwnProperty("Accept")||l.setRequestHeader("Accept","application/json, text/*");b.withCredentials&&(l.withCredentials=b.withCredentials);for(var z in b.headers)({}).hasOwnProperty.call(b.headers,z)&&l.setRequestHeader(z,b.headers[z]); "function"===typeof b.config&&(l=b.config(l,b)||l);l.onreadystatechange=function(){if(!y&&4===l.readyState)try{var a=b.extract!==q?b.extract(l,b):b.deserialize(b.extract(l,b));if(200<=l.status&&300>l.status||304===l.status||W.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:y(q)},setCompletionCallback:function(b){z=b}}}(window,p),R=function(a){function c(h, -d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=t&&x>=r;){var w= -d[t],p=a[r];if(w!==p||c)if(null==w)t++;else if(null==p)r++;else if(w.key===p.key){var C=null!=q&&t>=d.length-q.length||null==q&&c;t++;r++;m(h,w,p,e,z(d,t,g),C,k);c&&w.tag===p.tag&&b(h,l(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)r++;else if(w.key===p.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),C,k),(c||r=t&&x>=r;){w=d[v];p=a[x];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key=== -p.key)C=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),C,k),c&&w.tag===p.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;C=v;w={};var u;for(u=0;u=d.length-q.length||null==q&&c,m(h,u,p,e,z(d,v+1,g),C,k),b(h,l(u),g),d[w].skip=!0,null!=u.dom&&(g=u.dom)):g=n(h,p,e,k,g))}x--}else v--,x--;if(x=u&&D>=r;){var w= +d[u],p=a[r];if(w!==p||c)if(null==w)u++;else if(null==p)r++;else if(w.key===p.key){var t=null!=q&&u>=d.length-q.length||null==q&&c;u++;r++;m(h,w,p,e,z(d,u,g),t,k);c&&w.tag===p.tag&&b(h,l(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)r++;else if(w.key===p.key)t=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),t,k),(c||r=u&&D>=r;){w=d[v];p=a[D];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key=== +p.key)t=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),t,k),c&&w.tag===p.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-q.length||null==q&&c,m(h,B,p,e,z(d,v+1,g),t,k),b(h,l(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=n(h,p,e,k,g))}D--}else v--,D--;if(D Date: Sat, 4 Nov 2017 04:01:19 +1100 Subject: [PATCH 23/66] docs: Feature/testing docs update (#2012) --- docs/testing.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index ba850a5b..ead11517 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -74,7 +74,9 @@ var m = require("mithril") module.exports = { view: function() { - return m("div", "Hello world") + return m("div", + m("p", "Hello World") + ) } } ``` @@ -90,7 +92,7 @@ o.spec("MyComponent", function() { o(vnode.tag).equals("div") o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("#") + o(vnode.children[0].tag).equals("p") o(vnode.children[0].children).equals("Hello world") }) }) From e90f14ebe0a1005c1bde787bbbf5abea5348340b Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Mon, 13 Nov 2017 17:06:20 +0100 Subject: [PATCH 24/66] docs: Fix mailto link (#2015) --- docs/code-of-conduct.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md index 50ebe61d..a8875af5 100644 --- a/docs/code-of-conduct.md +++ b/docs/code-of-conduct.md @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [github@patcavit.com](mailto:github@patcavit.com?subject=Mithril Code of Conduct). All +reported by contacting the project team at [github@patcavit.com](mailto:github@patcavit.com?subject=Mithril%20Code%20of%20Conduct). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. From 80b6a1af0d02309167788ed7a95aa1f275e07653 Mon Sep 17 00:00:00 2001 From: spacejack Date: Mon, 13 Nov 2017 11:08:54 -0500 Subject: [PATCH 25/66] feat: Don't reject m.request Promise if extract callback supplied (#2006) --- docs/change-log.md | 1 + docs/request.md | 6 ++++-- request/request.js | 2 +- request/tests/test-request.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 087202a9..28aabf32 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -20,6 +20,7 @@ - API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) - API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) - API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook. +- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses. #### News diff --git a/docs/request.md b/docs/request.md index fe962e50..fb013df0 100644 --- a/docs/request.md +++ b/docs/request.md @@ -54,7 +54,7 @@ Argument | Type | Required | Descr `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.serialize` | `string = Function(any)` | No | A serialization method to be applied to `data`. Defaults to `JSON.stringify`, or if `options.data` is an instance of [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData), defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function) (i.e. `function(value) {return value}`). `options.deserialize` | `any = Function(string)` | No | A deserialization method to be applied to the `xhr.responseText`. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses. If `extract` is defined, `deserialize` will be skipped. -`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `xhr.responseText`, which is in turn passed to `deserialize`. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will not automatically be parsed as JSON. +`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `xhr.responseText`, which is in turn passed to `deserialize`. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will be left as-is when the promise resolves. Furthermore, when an extract callback is provided, exceptions are *not* thrown when the server response status code indicates an error. `options.useBody` | `Boolean` | No | Force the use of the HTTP body section for `data` in `GET` requests when set to `true`, or the use of querystring for other HTTP methods when set to `false`. Defaults to `false` for `GET` requests and `true` for other methods. `options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`. **returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods @@ -81,6 +81,8 @@ A call to `m.request` returns a [promise](promise.md) and triggers a redraw upon By default, `m.request` assumes the response is in JSON format and parses it into a Javascript object (or array). +If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection. + --- ### Typical usage @@ -426,7 +428,7 @@ m.request({ ### Retrieving response details -By default Mithril attempts to parse a response as JSON and returns `xhr.responseText`. It may be useful to inspect a server response in more detail, this can be accomplished by passing a custom `options.extract` function: +By default Mithril attempts to parse `xhr.responseText` as JSON and returns the parsed object. It may be useful to inspect a server response in more detail and process it manually. This can be accomplished by passing a custom `options.extract` function: ```javascript m.request({ diff --git a/request/request.js b/request/request.js index 7e4ec744..b5190ba7 100644 --- a/request/request.js +++ b/request/request.js @@ -88,7 +88,7 @@ module.exports = function($window, Promise) { if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { + if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 94e7e172..5ad5da91 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -519,5 +519,35 @@ o.spec("xhr", function() { o(e instanceof Error).equals(true) }).then(done) }) + o("does not reject on status error code when extract provided", function(done) { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 500, responseText: JSON.stringify({message: "error"})} + } + }) + xhr({ + method: "GET", url: "/item", + extract: function(xhr) {return JSON.parse(xhr.responseText)} + }).then(function(data) { + o(data.message).equals("error") + done() + }) + }) + o("rejects on error in extract", function(done) { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: JSON.stringify({a: 1})} + } + }) + xhr({ + method: "GET", url: "/item", + extract: function() {throw new Error("error")} + }).catch(function(e) { + o(e instanceof Error).equals(true) + o(e.message).equals("error") + }).then(function() { + done() + }) + }) }) }) From a382c85eb0754bc388275a2b8810c7f1c5347d26 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 13 Nov 2017 16:10:41 +0000 Subject: [PATCH 26/66] Bundled output for commit 80b6a1af0d02309167788ed7a95aa1f275e07653 [skip ci] --- README.md | 2 +- mithril.js | 2 +- mithril.min.js | 62 +++++++++++++++++++++++++------------------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 214d677a..0969834e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.45 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.46 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index 57faabb4..dd480518 100644 --- a/mithril.js +++ b/mithril.js @@ -304,7 +304,7 @@ var _8 = function($window, Promise) { if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { + if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { diff --git a/mithril.min.js b/mithril.min.js index 417ab72b..53007e6f 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,46 +1,46 @@ -(function(){function x(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function N(a){for(var c in a)if(C.call(a,c))return!1;return!0}function A(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=O[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var q=f[1], -m=f[2];""===q&&""!==m?n=m:"#"===q?g.id=m:"."===q?k.push(m):"["===f[3][0]&&((q=f[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(q):g[f[4]]=""===q?q:q||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function q(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cb.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function p(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||W.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b,g);var q=new c(function(c,e){var f=b.callbackName||"_mithril_"+ -Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?q:y(q)},setCompletionCallback:function(b){z=b}}}(window,p),R=function(a){function c(h, +"function"===typeof b.config&&(l=b.config(l,b)||l);l.onreadystatechange=function(){if(!y&&4===l.readyState)try{var a=b.extract!==p?b.extract(l,b):b.deserialize(b.extract(l,b));if(b.extract!==p||200<=l.status&&300>l.status||304===l.status||W.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b,g);var p=new c(function(c,e){var f=b.callbackName|| +"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?p:y(p)},setCompletionCallback:function(b){z=b}}}(window,q),R=function(a){function c(h, d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&D>=r;){var w= -d[u],p=a[r];if(w!==p||c)if(null==w)u++;else if(null==p)r++;else if(w.key===p.key){var t=null!=q&&u>=d.length-q.length||null==q&&c;u++;r++;m(h,w,p,e,z(d,u,g),t,k);c&&w.tag===p.tag&&b(h,l(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)r++;else if(w.key===p.key)t=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),t,k),(c||r=u&&D>=r;){w=d[v];p=a[D];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key=== -p.key)t=null!=q&&v>=d.length-q.length||null==q&&c,m(h,w,p,e,z(d,v+1,g),t,k),c&&w.tag===p.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-q.length||null==q&&c,m(h,B,p,e,z(d,v+1,g),t,k),b(h,l(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=n(h,p,e,k,g))}D--}else v--,D--;if(D=u&&D>=r;){var w= +d[u],q=a[r];if(w!==q||c)if(null==w)u++;else if(null==q)r++;else if(w.key===q.key){var t=null!=p&&u>=d.length-p.length||null==p&&c;u++;r++;m(h,w,q,e,z(d,u,g),t,k);c&&w.tag===q.tag&&b(h,l(w),g)}else if(w=d[v],w!==q||c)if(null==w)v--;else if(null==q)r++;else if(w.key===q.key)t=null!=p&&v>=d.length-p.length||null==p&&c,m(h,w,q,e,z(d,v+1,g),t,k),(c||r=u&&D>=r;){w=d[v];q=a[D];if(w!==q||c)if(null==w)v--;else{if(null!=q)if(w.key=== +q.key)t=null!=p&&v>=d.length-p.length||null==p&&c,m(h,w,q,e,z(d,v+1,g),t,k),c&&w.tag===q.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-p.length||null==p&&c,m(h,B,q,e,z(d,v+1,g),t,k),b(h,l(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=n(h,q,e,k,g))}D--}else v--,D--;if(D Date: Tue, 14 Nov 2017 23:47:02 +0100 Subject: [PATCH 27/66] feat: Add support for `timeout` to `m.request` (#1966) --- docs/change-log.md | 1 + docs/request.md | 1 + request/request.js | 2 ++ request/tests/test-request.js | 14 ++++++++++++++ 4 files changed, 18 insertions(+) diff --git a/docs/change-log.md b/docs/change-log.md index 28aabf32..ad28942a 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -27,6 +27,7 @@ - API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) - API: Event handlers may also be objects with `handleEvent` methods ([#1939](https://github.com/MithrilJS/mithril.js/issues/1939)). - API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930)) +- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966)) #### Ospec improvements: diff --git a/docs/request.md b/docs/request.md index fb013df0..44bb954b 100644 --- a/docs/request.md +++ b/docs/request.md @@ -49,6 +49,7 @@ Argument | Type | Required | Descr `options.user` | `String` | No | A username for HTTP authorization. Defaults to `undefined`. `options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network. `options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false` +`options.timeout` | `Number` | No | The amount of milliseconds a request can take before automatically being [terminated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout). Defaults to `undefined`. `options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`). `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). diff --git a/request/request.js b/request/request.js index b5190ba7..8424f889 100644 --- a/request/request.js +++ b/request/request.js @@ -75,6 +75,8 @@ module.exports = function($window, Promise) { } if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout + for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { xhr.setRequestHeader(key, args.headers[key]) } diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 5ad5da91..5c3c4cce 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -435,6 +435,20 @@ o.spec("xhr", function() { done() }) }) + o("set timeout to xhr instance", function() { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: ''} + } + }) + return xhr({ + method: "GET", url: "/item", + timeout: 42, + config: function(xhr) { + o(xhr.timeout).equals(42) + } + }) + }) /*o("data maintains after interpolate", function() { mock.$defineRoutes({ "PUT /items/:x": function() { From 684a394d97e8a4a4c630a3cd4819ff3779036a4a Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Tue, 14 Nov 2017 22:48:49 +0000 Subject: [PATCH 28/66] Bundled output for commit d50d53f31d384b0eac83d4ea24dbe43c96e941fb [skip ci] --- README.md | 2 +- mithril.js | 1 + mithril.min.js | 92 +++++++++++++++++++++++++------------------------- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 0969834e..b9240f22 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.46 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.48 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index dd480518..182ca224 100644 --- a/mithril.js +++ b/mithril.js @@ -294,6 +294,7 @@ var _8 = function($window, Promise) { xhr.setRequestHeader("Accept", "application/json, text/*") } if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { xhr.setRequestHeader(key, args.headers[key]) } diff --git a/mithril.min.js b/mithril.min.js index 53007e6f..8c155377 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,46 +1,46 @@ -(function(){function x(a,c,e,f,n,k){return{tag:a,key:c,attrs:e,children:f,text:n,dom:k,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function N(a){for(var c in a)if(C.call(a,c))return!1;return!0}function A(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=O[a])){var n="div";for(var k=[],g={};f=S.exec(a);){var p=f[1], -m=f[2];""===p&&""!==m?n=m:"#"===p?g.id=m:"."===p?k.push(m):"["===f[3][0]&&((p=f[6])&&(p=p.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?k.push(p):g[f[4]]=""===p?p:p||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function p(b){return b.responseText}function m(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;cl.status||304===l.status||W.test(b.url))c(m(b.type,a));else{var f=Error(l.responseText);f.code=l.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?l.send(b.data):l.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b,g);var p=new c(function(c,e){var f=b.callbackName|| -"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(m(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=n(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=k(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?p:y(p)},setCompletionCallback:function(b){z=b}}}(window,q),R=function(a){function c(h, -d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&D>=r;){var w= -d[u],q=a[r];if(w!==q||c)if(null==w)u++;else if(null==q)r++;else if(w.key===q.key){var t=null!=p&&u>=d.length-p.length||null==p&&c;u++;r++;m(h,w,q,e,z(d,u,g),t,k);c&&w.tag===q.tag&&b(h,l(w),g)}else if(w=d[v],w!==q||c)if(null==w)v--;else if(null==q)r++;else if(w.key===q.key)t=null!=p&&v>=d.length-p.length||null==p&&c,m(h,w,q,e,z(d,v+1,g),t,k),(c||r=u&&D>=r;){w=d[v];q=a[D];if(w!==q||c)if(null==w)v--;else{if(null!=q)if(w.key=== -q.key)t=null!=p&&v>=d.length-p.length||null==p&&c,m(h,w,q,e,z(d,v+1,g),t,k),c&&w.tag===q.tag&&b(h,l(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-p.length||null==p&&c,m(h,B,q,e,z(d,v+1,g),t,k),b(h,l(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=n(h,q,e,k,g))}D--}else v--,D--;if(Db.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function n(b){return b.responseText}function r(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;ck.status||304===k.status||W.test(b.url))c(r(b.type,a));else{var f=Error(k.responseText);f.code=k.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b, +g);var n=new c(function(c,e){var f=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(r(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=m(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=l(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?n:y(n)},setCompletionCallback:function(b){z= +b}}}(window,p),R=function(a){function c(h,d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&D>=q;){var w=d[u],p=a[q];if(w!==p||c)if(null==w)u++;else if(null==p)q++;else if(w.key===p.key){var t=null!=n&&u>=d.length-n.length||null==n&&c;u++;q++;r(h,w,p,e,z(d,u,g),t,l);c&&w.tag===p.tag&&b(h,k(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)q++;else if(w.key===p.key)t=null!=n&&v>=d.length-n.length||null==n&&c,r(h,w,p,e,z(d,v+1,g),t,l),(c||q=u&&D>=q;){w=d[v];p=a[D];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key===p.key)t=null!=n&&v>=d.length-n.length||null==n&&c,r(h,w,p,e,z(d,v+1,g),t,l),c&&w.tag===p.tag&&b(h,k(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-n.length||null==n&&c,r(h,B,p,e,z(d,v+1,g),t,l),b(h,k(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=m(h,p,e, +l,g))}D--}else v--,D--;if(D Date: Mon, 20 Nov 2017 17:30:08 -0500 Subject: [PATCH 29/66] docs: Learning section (#1817) --- docs/learning-mithril.md | 5 +++++ docs/nav-guides.md | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/learning-mithril.md diff --git a/docs/learning-mithril.md b/docs/learning-mithril.md new file mode 100644 index 00000000..fb599df8 --- /dev/null +++ b/docs/learning-mithril.md @@ -0,0 +1,5 @@ +# Learning Mithril + +Links to Mithril learning content: + +- [Mithril 0-60](https://scrimba.com/playlist/playlist-34) diff --git a/docs/nav-guides.md b/docs/nav-guides.md index 872ce86b..4a5be9df 100644 --- a/docs/nav-guides.md +++ b/docs/nav-guides.md @@ -9,6 +9,7 @@ - [Animation](animation.md) - [Testing](testing.md) - [Examples](examples.md) + - [Learning Mithril](learning-mithril.md) - Key concepts - [Vnodes](vnodes.md) - [Components](components.md) From 78eeb2b365a300905d038b72dd3fcbe369ee77aa Mon Sep 17 00:00:00 2001 From: Sage Gerard Date: Mon, 20 Nov 2017 18:37:07 -0500 Subject: [PATCH 30/66] feat(ospec): Allow custom reporters for CI reasons (#2019) (#2020) --- docs/change-log.md | 1 + ospec/README.md | 84 +++++++++++++++++++++++++++++++++++++-- ospec/ospec.js | 8 +++- ospec/tests/test-ospec.js | 34 ++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index ad28942a..891107fa 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -32,6 +32,7 @@ #### Ospec improvements: - Added support for async functions and promises in tests - ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) +- Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) - Error handling for async tests with `done` callbacks supports error as first argument - Error messages which include newline characters do not swallow the stack trace ([#1495](https://github.com/MithrilJS/mithril.js/issues/1495)) diff --git a/ospec/README.md b/ospec/README.md index dc3d923e..f2610e0f 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -19,7 +19,7 @@ Noiseless testing framework - `before`/`after`/`beforeEach`/`afterEach` hooks - test exclusivity (i.e. `.only`) - async tests and hooks -- explicitly disallows test-space configuration to encourage focus on testing, and to provide uniform test suites across projects +- explicitly regulates test-space configuration to encourage focus on testing, and to provide uniform test suites across projects ## Usage @@ -437,9 +437,17 @@ The arguments that were passed to the function in the last time it was called --- -### void o.run() +### void o.run([Function reporter]) -Runs the test suite +Runs the test suite. By default passing test results are printed using +`console.log` and failing test results are printed using `console.error`. + +If you have custom continuous integration needs then you can use a +reporter to process [test result data](#result-data) yourself. + +If running in Node.js, ospec will call `process.exit` after reporting +results by default. If you specify a reporter, ospec will not do this +and allow your reporter to respond to results in its own way. --- @@ -455,6 +463,74 @@ $o("a test", function() { $o.run() ``` +--- + +## Result data + +Test results are available by reference for integration purposes. You +can use custom reporters in `o.run()` to process these results. + +```javascript +o.run(function(results) { + // results is an array + + results.forEach(function(result) { + // ... + }) +}) +``` + +--- + +### Boolean result.pass + +True if the test passed. **No other keys will exist on the result if this value is true.** + +--- + +### Error result.error + +The value of the stack property from the `Error` object explaining the reason behind a failure. + +--- + +### String result.message + +If an exception was thrown inside the corresponding test, this will equal that Error's `message`. Otherwise, this will be a preformatted message in [SVO form](https://en.wikipedia.org/wiki/Subject%E2%80%93verb%E2%80%93object). More specifically, `${subject}\n${verb}\n${object}`. + +As an example, the following test's result message will be `"false\nshould equal\ntrue"`. + +```javascript +o.spec("message", function() { + o(false).equals(true) +}) +``` + +If you specify an assertion description, that description will appear two lines above the subject. + +```javascript +o.spec("message", function() { + o(false).equals(true)("Candyland") // result.message === "Candyland\n\nfalse\nshould equal\ntrue" +}) +``` + +--- + +### String result.context + +A `>`-separated string showing the structure of the test specification. +In the below example, `result.context` would be `testing > rocks`. + +```javascript +o.spec("testing", function() { + o.spec("rocks", function() { + o(false).equals(true) + }) +}) +``` + + + --- ## Goals @@ -462,8 +538,8 @@ $o.run() - Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies - Disallow configuration in test-space: - Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) - - Disallow ability to pick between different reporters - Disallow ability to add custom assertion types + - Provide a default simple reporter - Make assertion code terse, readable and self-descriptive - Have as few assertion types as possible for a workable usage pattern diff --git a/ospec/ospec.js b/ospec/ospec.js index b1036743..5b29ec84 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -2,7 +2,7 @@ "use strict" module.exports = new function init(name) { - var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty + var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", reporter, hasOwn = ({}).hasOwnProperty if (name != null) spec[name] = ctx = {} @@ -52,9 +52,10 @@ module.exports = new function init(name) { o.cleanStackTrace = function(stack) { return stack.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/gm).pop() } - o.run = function() { + o.run = function(_reporter) { results = [] start = new Date + reporter = _reporter test(spec, [], [], report) function test(spec, pre, post, finalize) { @@ -236,6 +237,9 @@ module.exports = new function init(name) { function report() { var status = 0 + + if (typeof reporter === "function") return reporter(results) + for (var i = 0, r; r = results[i]; i++) { if (!r.pass) { var stackTrace = o.cleanStackTrace(r.error) diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 4ef4614d..a1894419 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -18,6 +18,40 @@ new function(o) { o.run() }(o) +new function(o) { + var clone = o.new() + + clone.spec("clone", function() { + clone("fail", function() { + clone(true).equals(false) + }) + + clone("pass", function() { + clone(true).equals(true) + }) + }) + + // Predicate test passing on clone results + o.spec("reporting", function() { + o("reports per instance", function(done, timeout) { + timeout(100) // Waiting on clone + + clone.run(function(results) { + o(typeof results).equals("object") + o("length" in results).equals(true) + o(results.length).equals(2)("Two results") + + o("error" in results[0] && "pass" in results[0]).equals(true)("error and pass keys present in failing result") + o(!("error" in results[1]) && "pass" in results[1]).equals(true)("only pass key present in passing result") + o(results[0].pass).equals(false)("Test meant to fail has failed") + o(results[1].pass).equals(true)("Test meant to pass has passed") + + done() + }) + }) + }) +}(o) + o.spec("ospec", function() { o.spec("sync", function() { var a = 0, b = 0, illegalAssertionThrows = false From 1be2c419817d1cbfb2e8a40e6d682c285ed2cd3a Mon Sep 17 00:00:00 2001 From: Andrea Coiutti Date: Fri, 24 Nov 2017 20:12:34 +0100 Subject: [PATCH 31/66] Fix headings displaying over menu on mobile (#2022) --- docs/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/style.css b/docs/style.css index bf0e6874..96efe7d2 100644 --- a/docs/style.css +++ b/docs/style.css @@ -57,7 +57,7 @@ h1 + ul strong + ul {border-left:3px solid #1e5799;} .hamburger:hover {text-decoration:none;} main section {margin:0;} header section {margin:0 0 20px;position:static;width:auto;} - h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;} + h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;z-index:1} h1 + ul + hr {display:block;} .navigating h1 + ul {display:block;} .navigating {overflow:hidden;} From d116f249db91c8c2ab233af7d93ce894c285e197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 17:08:46 +0100 Subject: [PATCH 32/66] lint: fix quotes --- ospec/tests/test-ospec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index a1894419..e837049d 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -183,13 +183,13 @@ o.spec("ospec", function() { }) }) - o.spec('stack trace cleaner', function() { - o('handles line breaks', function() { + o.spec("stack trace cleaner", function() { + o("handles line breaks", function() { try { - throw new Error('line\nbreak') + throw new Error("line\nbreak") } catch(error) { var trace = o.cleanStackTrace(error.stack) - o(trace).notEquals('break') + o(trace).notEquals("break") o(trace.includes("test-ospec.js")).equals(true) } }) From 4e35aff18881fb9c458d1cb19b5cb043ed593850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 21:39:35 +0100 Subject: [PATCH 33/66] Lint: Fix quotes in m.request tests (#2033) --- request/tests/test-request.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 5c3c4cce..52f840ef 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -230,7 +230,7 @@ o.spec("xhr", function() { } }) xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("deserialize parameter works in POST", function(done) { @@ -244,7 +244,7 @@ o.spec("xhr", function() { } }) xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("extract parameter works in GET", function(done) { @@ -258,7 +258,7 @@ o.spec("xhr", function() { } }) xhr({method: "GET", url: "/item", extract: extract}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("extract parameter works in POST", function(done) { @@ -272,7 +272,7 @@ o.spec("xhr", function() { } }) xhr({method: "POST", url: "/item", extract: extract}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("ignores deserialize if extract is defined", function(done) { @@ -438,7 +438,7 @@ o.spec("xhr", function() { o("set timeout to xhr instance", function() { mock.$defineRoutes({ "GET /item": function() { - return {status: 200, responseText: ''} + return {status: 200, responseText: ""} } }) return xhr({ From 12da6ea94af6238e13e125a659b34b9629565678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 22:14:36 +0100 Subject: [PATCH 34/66] Sync change log with v1.1.6 (#2035) --- docs/change-log.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 891107fa..0fccbf1d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -29,12 +29,6 @@ - API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930)) - API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966)) -#### Ospec improvements: - -- Added support for async functions and promises in tests - ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) -- Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) -- Error handling for async tests with `done` callbacks supports error as first argument -- Error messages which include newline characters do not swallow the stack trace ([#1495](https://github.com/MithrilJS/mithril.js/issues/1495)) #### Bug fixes @@ -45,12 +39,26 @@ - core: `Object.prototype` properties can no longer interfere with event listener calls. - API: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed. - core: `xlink:href` attributes are now correctly removed -- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks - render: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992) - render: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) --- +### v1.1.6 + +- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks ([#1988](https://github.com/MithrilJS/mithril.js/pull/1988), [@purplecode](https://github.com/purplecode)) +- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922)) +- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942)) + +#### Ospec improvements + +- Added support for async functions and promises in tests - ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) +- Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) +- Error handling for async tests with `done` callbacks supports error as first argument +- Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@ RodericDay](https://github.com/RodericDay)) + +--- + ### v1.1.5 - API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924)) @@ -61,9 +69,7 @@ #### Bug fixes: -- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922)) -- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942)) -- Fix IE bug where active element is null causing render function to throw error. ([1943](https://github.com/MithrilJS/mithril.js/pull/1943)) +- Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN)) #### Ospec improvements: From f06510bb62d44638addb8e1c0f0265584602fd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 21:46:26 +0100 Subject: [PATCH 35/66] Make ospec work natively in browser environments --- ospec/ospec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 5b29ec84..51e60e55 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -1,7 +1,9 @@ /* eslint-disable global-require, no-bitwise, no-process-exit */ "use strict" - -module.exports = new function init(name) { +;(function(m) { +if (typeof module !== "undefined") module["exports"] = m() +else window.o = m() +})(function init(name) { var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", reporter, hasOwn = ({}).hasOwnProperty if (name != null) spec[name] = ctx = {} @@ -265,4 +267,4 @@ module.exports = new function init(name) { } return o -} +}) From b38ba7c934442289b5e5a3be3e0587b1afe3ca84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 21:50:34 +0100 Subject: [PATCH 36/66] Pop `report()` off the stack (for better Flems output) --- ospec/ospec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 51e60e55..a538515a 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -58,7 +58,7 @@ else window.o = m() results = [] start = new Date reporter = _reporter - test(spec, [], [], report) + test(spec, [], [], function() {setTimeout(report)}) function test(spec, pre, post, finalize) { pre = [].concat(pre, spec["__beforeEach"] || []) From 8eb12454caf66b5a7fdadb0ff2e301f1cc02d179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 29 Nov 2017 22:17:42 +0100 Subject: [PATCH 37/66] update change log for #2034 --- docs/change-log.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/change-log.md b/docs/change-log.md index 0fccbf1d..cfaa74e9 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -56,6 +56,7 @@ - Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) - Error handling for async tests with `done` callbacks supports error as first argument - Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@ RodericDay](https://github.com/RodericDay)) +- Make Ospec more [Flems](https://flems.io)-friendly (#2034) --- From 0873cb53cc9872bf389e0f4356e33508d64710af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 00:27:08 +0100 Subject: [PATCH 38/66] Make the stack trace filter path-independent (auto-detection vs. hardcoded previously) --- ospec/ospec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index a538515a..29f7fdef 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -8,6 +8,13 @@ else window.o = m() if (name != null) spec[name] = ctx = {} + try {throw new Error} catch (e) { + var stackTraceMatcher = new RegExp( + "^(?:(?!Error|" + + e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] + + ").)*$", "gm" + ) + } function o(subject, predicate) { if (predicate === undefined) { if (results == null) throw new Error("Assertions should not occur outside test definitions") @@ -52,7 +59,7 @@ else window.o = m() return spy } o.cleanStackTrace = function(stack) { - return stack.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/gm).pop() + return stack.match(stackTraceMatcher).pop() } o.run = function(_reporter) { results = [] From cd3c1afca29d872ac8c5f5288157f3485853cacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 00:27:48 +0100 Subject: [PATCH 39/66] Readme: adjust the LOC count --- ospec/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ospec/README.md b/ospec/README.md index f2610e0f..7a257b31 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -7,7 +7,7 @@ Noiseless testing framework ## About -- ~180 LOC +- ~330 LOC including the CLI runner - terser and faster test code than with mocha, jasmine or tape - test code reads like bullet points - assertion code follows [SVO](https://en.wikipedia.org/wiki/Subject–verb–object) structure in present tense for terseness and readability From 9083a325a4ac6b7ebfe2657e8c94ef1663f2a057 Mon Sep 17 00:00:00 2001 From: Milan Lesichkov Date: Thu, 30 Nov 2017 07:05:21 +0000 Subject: [PATCH 40/66] docs: Fixed background issue (#2037) Fixed missing background for body, which causes browser default to apply, resulting in unreadable content --- docs/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/style.css b/docs/style.css index 96efe7d2..f75ea282 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,4 +1,4 @@ -body {-webkit-text-size-adjust: 100%;} +body {bakground:white;-webkit-text-size-adjust: 100%;} body,table,h5 {font:normal 16px 'Open Sans';} header,main {margin:auto;max-width:1000px;} header section {position:absolute;width:250px;} From 2fb828783a1a96794e4deeb0f74ce5bc58b7a5b9 Mon Sep 17 00:00:00 2001 From: Leo Thorp Date: Thu, 30 Nov 2017 02:25:10 -0600 Subject: [PATCH 41/66] fix typo in body background style --- docs/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/style.css b/docs/style.css index f75ea282..3da4e572 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,4 +1,4 @@ -body {bakground:white;-webkit-text-size-adjust: 100%;} +body {background:white;-webkit-text-size-adjust: 100%;} body,table,h5 {font:normal 16px 'Open Sans';} header,main {margin:auto;max-width:1000px;} header section {position:absolute;width:250px;} From b84e09369e6c78b4d6b29991956f81a3823057cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 01:36:47 +0100 Subject: [PATCH 42/66] ospec: better stack trace filter, fix #2036 --- ospec/README.md | 2 +- ospec/ospec.js | 24 ++++++++++++++++-------- ospec/tests/test-ospec.js | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ospec/README.md b/ospec/README.md index 7a257b31..b3a3ff76 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -490,7 +490,7 @@ True if the test passed. **No other keys will exist on the result if this value ### Error result.error -The value of the stack property from the `Error` object explaining the reason behind a failure. +The `Error` object explaining the reason behind a failure. --- diff --git a/ospec/ospec.js b/ospec/ospec.js index 29f7fdef..1c7cb94c 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -9,11 +9,7 @@ else window.o = m() if (name != null) spec[name] = ctx = {} try {throw new Error} catch (e) { - var stackTraceMatcher = new RegExp( - "^(?:(?!Error|" - + e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] - + ").)*$", "gm" - ) + var ospecFileName = e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] } function o(subject, predicate) { if (predicate === undefined) { @@ -58,8 +54,19 @@ else window.o = m() spy.callCount = 0 return spy } - o.cleanStackTrace = function(stack) { - return stack.match(stackTraceMatcher).pop() + o.cleanStackTrace = function(error) { + var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack + // some environments add the name and message to the stack trace + if (error.stack.indexOf(header) === 0) { + stack = error.stack.slice(header.length).split(/\r?\n/) + stack.shift() // drop the initial empty string + } else { + stack = error.stack.split(/\r?\n/) + } + // skip ospec-related entries on the stack + while (stack[i].indexOf(ospecFileName) !== -1) i++ + // now we're in user code + return stack[i] } o.run = function(_reporter) { results = [] @@ -230,7 +237,8 @@ else window.o = m() } result.context = subjects.join(" > ") result.message = message - result.error = error.stack + result.error = error + } results.push(result) } diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index e837049d..86218c83 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -188,7 +188,7 @@ o.spec("ospec", function() { try { throw new Error("line\nbreak") } catch(error) { - var trace = o.cleanStackTrace(error.stack) + var trace = o.cleanStackTrace(error) o(trace).notEquals("break") o(trace.includes("test-ospec.js")).equals(true) } From acd08c96dd6c2e6f3dcf3f7016e2ab1241e0ddcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 22:37:16 +0100 Subject: [PATCH 43/66] Make the stack trace cleaner IE9 compatible (err.stack is null) --- ospec/ospec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 1c7cb94c..4ab57ab3 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -9,7 +9,7 @@ else window.o = m() if (name != null) spec[name] = ctx = {} try {throw new Error} catch (e) { - var ospecFileName = e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] + var ospecFileName = e.stack && (/[\/\\](.*?):\d+:\d+/).test(e.stack) ? e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] : null } function o(subject, predicate) { if (predicate === undefined) { @@ -55,6 +55,8 @@ else window.o = m() return spy } o.cleanStackTrace = function(error) { + // For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack + if (error.stack == null) return "" var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack // some environments add the name and message to the stack trace if (error.stack.indexOf(header) === 0) { @@ -63,6 +65,7 @@ else window.o = m() } else { stack = error.stack.split(/\r?\n/) } + if (ospecFileName == null) return stack.join("\n") // skip ospec-related entries on the stack while (stack[i].indexOf(ospecFileName) !== -1) i++ // now we're in user code From 769c854f8247c9271f404f82869892e62ab1efa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 23:25:31 +0100 Subject: [PATCH 44/66] Expose the reporter as o.report(results), have it return the number of errors --- ospec/ospec.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ospec/ospec.js b/ospec/ospec.js index 4ab57ab3..9769bccd 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -4,7 +4,7 @@ if (typeof module !== "undefined") module["exports"] = m() else window.o = m() })(function init(name) { - var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", reporter, hasOwn = ({}).hasOwnProperty + var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty if (name != null) spec[name] = ctx = {} @@ -71,11 +71,18 @@ else window.o = m() // now we're in user code return stack[i] } - o.run = function(_reporter) { + o.run = function(reporter) { results = [] start = new Date - reporter = _reporter - test(spec, [], [], function() {setTimeout(report)}) + test(spec, [], [], function() { + setTimeout(function () { + if (typeof reporter === "function") reporter(results) + else { + var errCount = o.report(results) + if (hasProcess && errCount !== 0) process.exit(1) + } + }) + }) function test(spec, pre, post, finalize) { pre = [].concat(pre, spec["__beforeEach"] || []) @@ -255,16 +262,13 @@ else window.o = m() return hasProcess ? "\x1b[31m" + message + "\x1b[0m" : "%c" + message + "%c " } - function report() { - var status = 0 - - if (typeof reporter === "function") return reporter(results) - + o.report = function (results) { + var errCount = 0 for (var i = 0, r; r = results[i]; i++) { if (!r.pass) { var stackTrace = o.cleanStackTrace(r.error) console.error(r.context + ":\n" + highlight(r.message) + (stackTrace ? "\n\n" + stackTrace + "\n\n" : ""), hasProcess ? "" : "color:red", hasProcess ? "" : "color:black") - status = 1 + errCount++ } } console.log( @@ -272,10 +276,10 @@ else window.o = m() results.length + " assertions completed in " + Math.round(new Date - start) + "ms, " + "of which " + results.filter(function(result){return result.error}).length + " failed" ) - if (hasProcess && status === 1) process.exit(1) + return errCount } - if(hasProcess) { + if (hasProcess) { nextTickish = process.nextTick } else { nextTickish = function fakeFastNextTick(next) { From 3ac17d0075281f39dc3df0bfa53158ecdd5ff65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 30 Nov 2017 23:49:34 +0100 Subject: [PATCH 45/66] ospec: tests and docs for o.report --- ospec/README.md | 11 ++++++++++- ospec/tests/test-ospec.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/ospec/README.md b/ospec/README.md index b3a3ff76..9b0b416c 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -311,7 +311,9 @@ ospec will automatically evaluate all `*.js` files in any folder named `/tests`. Ospec doesn't work when installed globally. Using global scripts is generally a bad idea since you can end up with different, incompatible versions of the same package installed locally and globally. -To work around this limitation, you can use [`npm-run`](https://www.npmjs.com/package/npm-run) which enables one to run the binaries of locally installed packages. +If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder. + +Otherwise, to work around this limitation, you can use [`npm-run`](https://www.npmjs.com/package/npm-run) which enables one to run the binaries of locally installed packages. ``` npm install npm-run -g @@ -449,6 +451,13 @@ If running in Node.js, ospec will call `process.exit` after reporting results by default. If you specify a reporter, ospec will not do this and allow your reporter to respond to results in its own way. + +--- + +### Number o.report(results) + +The default reporter used by `o.run()` when none are provided. Returns the number of failures, doesn't exit Node.js by itself. It expects an array of [test result data](#result-data) as argument. + --- ### Function o.new() diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 86218c83..526671c6 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -49,6 +49,43 @@ new function(o) { done() }) }) + o("o.report() returns the number of failures", function () { + var log = console.log, error = console.error + console.log = o.spy() + console.error = o.spy() + + function makeError(msg) {try{throw msg ? new Error(msg) : new Error} catch(e){return e}} + try { + var errCount = o.report([{pass: true}, {pass: true}]) + + o(errCount).equals(0) + o(console.log.callCount).equals(1) + o(console.error.callCount).equals(0) + + errCount = o.report([ + {pass: false, error: makeError("hey"), message: "hey"} + ]) + + o(errCount).equals(1) + o(console.log.callCount).equals(2) + o(console.error.callCount).equals(1) + + errCount = o.report([ + {pass: false, error: makeError("hey"), message: "hey"}, + {pass: true}, + {pass: false, error: makeError("ho"), message: "ho"} + ]) + + o(errCount).equals(2) + o(console.log.callCount).equals(3) + o(console.error.callCount).equals(3) + } catch (e) { + o(1).equals(0)("Error while testing the reporter") + } + + console.log = log + console.error = error + }) }) }(o) From 854021db32ca7a8644c603f639db8eba925cd17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Fri, 1 Dec 2017 14:01:55 +0100 Subject: [PATCH 46/66] Detail the upcoming ospec v2 additions --- docs/change-log.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index cfaa74e9..69e49a26 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -52,11 +52,18 @@ #### Ospec improvements -- Added support for async functions and promises in tests - ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) -- Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) -- Error handling for async tests with `done` callbacks supports error as first argument -- Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@ RodericDay](https://github.com/RodericDay)) -- Make Ospec more [Flems](https://flems.io)-friendly (#2034) +- ospec v1.4.0 + - Added support for async functions and promises in tests ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928), [@StephanHoyer](https://github.com/StephanHoyer)) + - Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) + - Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay)) +- ospec v2.0.0 (to be released) + - Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) + - Make Ospec more [Flems](https://flems.io)-friendly (#2034) + - Works either as a global or in CommonJS environments + - the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async). + - Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036) + - expose the default reporter as `o.report(results)` + - Don't try to access the stack traces in IE9 --- From 6f435a55f9232ecaae269a5ee4e2b65cb022f8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Fri, 1 Dec 2017 14:12:17 +0100 Subject: [PATCH 47/66] More change log tweaks --- docs/change-log.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 69e49a26..02eb18ac 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,6 +1,7 @@ # Change log - [v2.0.0](#v200-wip) +- [v1.1.6](#v115) - [v1.1.5](#v115) - [v1.1.4](#v114) - [v1.1.3](#v113) @@ -47,8 +48,8 @@ ### v1.1.6 - core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks ([#1988](https://github.com/MithrilJS/mithril.js/pull/1988), [@purplecode](https://github.com/purplecode)) -- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922)) -- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942)) +- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [@octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922)) +- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [@s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942)) #### Ospec improvements From 84b729d012e9667dd74357b9c4d4d59065020781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Fri, 1 Dec 2017 14:27:32 +0100 Subject: [PATCH 48/66] And some more change log tweaks --- docs/change-log.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/change-log.md b/docs/change-log.md index 02eb18ac..28e8173c 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -58,13 +58,13 @@ - Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) - Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay)) - ospec v2.0.0 (to be released) - - Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) - - Make Ospec more [Flems](https://flems.io)-friendly (#2034) - - Works either as a global or in CommonJS environments - - the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async). - - Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036) - - expose the default reporter as `o.report(results)` - - Don't try to access the stack traces in IE9 + - Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) + - Make Ospec more [Flems](https://flems.io)-friendly ([#2034](https://github.com/MithrilJS/mithril.js/pull/2034)) + - Works either as a global or in CommonJS environments + - the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async). + - Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036) + - expose the default reporter as `o.report(results)` + - Don't try to access the stack traces in IE9 --- From 8dc21f4c481a073f18cd33796f863bb9f9aa98bc Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Fri, 1 Dec 2017 17:05:45 +0100 Subject: [PATCH 49/66] docs: Fix anchor target (#2042) --- docs/change-log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 28e8173c..a7d3b220 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,7 +1,7 @@ # Change log - [v2.0.0](#v200-wip) -- [v1.1.6](#v115) +- [v1.1.6](#v116) - [v1.1.5](#v115) - [v1.1.4](#v114) - [v1.1.3](#v113) From 12d3540d476ae73d2c46dfa75249b5a1b21b2030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Sat, 2 Dec 2017 00:51:32 +0100 Subject: [PATCH 50/66] domMock: implement event propagation --- test-utils/domMock.js | 127 ++++++-- test-utils/tests/test-domMock.js | 542 ++++++++++++++++++++++++++++++- 2 files changed, 648 insertions(+), 21 deletions(-) diff --git a/test-utils/domMock.js b/test-utils/domMock.js index b8f5d1a3..a4c7c598 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -37,6 +37,30 @@ module.exports = function(options) { function isModernEvent(type) { return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend" } + function dispatchEvent(e) { + var stopped = false + e.stopImmediatePropagation = function() { + e.stopPropagation() + stopped = true + } + e.currentTarget = this + if (this._events[e.type] != null) { + for (var i = 0; i < this._events[e.type].handlers.length; i++) { + var useCapture = this._events[e.type].options[i].capture + if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) { + var handler = this._events[e.type].handlers[i] + if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})} + else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})} + if (stopped) return + } + } + } + // this is inaccurate. Normally the event fires in definition order, including legacy events + // this would require getters/setters for each of them though and we haven't gotten around to + // adding them since it would be at a high perf cost or would entail some heavy refactoring of + // the mocks (prototypes instead of closures). + if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})} + } function appendChild(child) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode @@ -260,35 +284,99 @@ module.exports = function(options) { else this.setAttribute("class", value) }, focus: function() {activeElement = this}, - addEventListener: function(type, callback) { - if (events[type] == null) events[type] = [callback] - else events[type].push(callback) + addEventListener: function(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } + if (events[type] == null) events[type] = {handlers: [handler], options: [options]} + else { + var found = false + for (var i = 0; i < events[type].handlers.length; i++) { + if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { + found = true + break + } + } + if (!found) { + events[type].handlers.push(handler) + events[type].options.push(options) + } + } }, - removeEventListener: function(type, callback) { + removeEventListener: function(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } if (events[type] != null) { - var index = events[type].indexOf(callback) - if (index > -1) events[type].splice(index, 1) + for (var i = 0; i < events[type].handlers.length; i++) { + if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { + events[type].handlers.splice(i, 1) + events[type].options.splice(i, 1) + break; + } + } } }, dispatchEvent: function(e) { - if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") { - this.checked = !this.checked + var parents = [] + if (this.parentNode != null) { + var parent = this.parentNode + do { + parents.push(parent) + parent = parent.parentNode + } while (parent != null) } - e.target = this - if (events[e.type] != null) { - for (var i = 0; i < events[e.type].length; i++) { - var handler = events[e.type][i] - if (typeof handler === "function") handler.call(this, e) - else handler.handleEvent(e) + var prevented = false + e.preventDefault = function() { + prevented = true + } + var stopped = false + e.stopPropagation = function() { + stopped = true + } + e.eventPhase = 1 + try { + for (var i = parents.length - 1; 0 <= i; i--) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + e.eventPhase = 2 + dispatchEvent.call(this, e) + if (stopped) { + return + } + e.eventPhase = 3 + for (var i = 0; i < parents.length; i++) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + } catch(e) { + throw e + } finally { + e.eventPhase = 0 + if (!prevented) { + if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") { + this.checked = !this.checked + } } } - e.preventDefault = function() { - // TODO: should this do something? - } - if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e) + }, onclick: null, + _events: events } if (element.nodeName === "A") { @@ -515,7 +603,8 @@ module.exports = function(options) { }, createEvent: function() { return { - initEvent: function(type) {this.type = type}, + eventPhase: 0, + initEvent: function(type) {this.type = type} } }, get activeElement() {return activeElement}, diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js index 3e4a9567..f05d24c9 100644 --- a/test-utils/tests/test-domMock.js +++ b/test-utils/tests/test-domMock.js @@ -611,13 +611,57 @@ o.spec("domMock", function() { o(spy.args[0].type).equals("click") o(spy.args[0].target).equals(div) }) - o("removeEventListener works", function(done) { + o("removeEventListener works (bubbling phase)", function() { div.addEventListener("click", spy, false) div.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(0) - done() + }) + o("removeEventListener works (capture phase)", function() { + div.addEventListener("click", spy, true) + div.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + o("removeEventListener is selective (bubbling phase)", function() { + var other = o.spy() + div.addEventListener("click", spy, false) + div.addEventListener("click", other, false) + div.removeEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + o(other.callCount).equals(1) + }) + o("removeEventListener is selective (capture phase)", function() { + var other = o.spy() + div.addEventListener("click", spy, true) + div.addEventListener("click", other, true) + div.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + o(other.callCount).equals(1) + }) + o("removeEventListener only removes the handler related to a given phase (1/2)", function() { + spy = o.spy(function(e) {o(e.eventPhase).equals(3)}) + $document.body.addEventListener("click", spy, true) + $document.body.addEventListener("click", spy, false) + $document.body.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(1) + }) + o("removeEventListener only removes the handler related to a given phase (2/2)", function() { + spy = o.spy(function(e) {o(e.eventPhase).equals(1)}) + $document.body.addEventListener("click", spy, true) + $document.body.addEventListener("click", spy, false) + $document.body.removeEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(1) }) o("click fires onclick", function() { div.onclick = spy @@ -655,6 +699,488 @@ o.spec("domMock", function() { done() }) }) + o.spec("capture and bubbling phases", function() { + var div, e + o.beforeEach(function() { + div = $document.createElement("div") + e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + $document.body.appendChild(div) + }) + o.afterEach(function() { + $document.body.removeChild(div) + }) + o("capture and bubbling events both fire on the target in the order they were defined, regardless of the phase", function () { + var sequence = [] + var capture = o.spy(function(ev){ + sequence.push("capture") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + var bubble = o.spy(function(ev){ + sequence.push("bubble") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + div.addEventListener("click", bubble, false) + div.addEventListener("click", capture, true) + div.dispatchEvent(e) + + o(capture.callCount).equals(1) + o(bubble.callCount).equals(1) + o(sequence).deepEquals(["bubble", "capture"]) + }) + o("capture and bubbling events both fire on the parent", function () { + var sequence = [] + var capture = o.spy(function(ev){ + sequence.push("capture") + + o(ev).equals(e) + o(ev.eventPhase).equals(1) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var bubble = o.spy(function(ev){ + sequence.push("bubble") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + + $document.body.addEventListener("click", bubble, false) + $document.body.addEventListener("click", capture, true) + div.dispatchEvent(e) + + o(capture.callCount).equals(1) + o(bubble.callCount).equals(1) + o(sequence).deepEquals(["capture", "bubble"]) + }) + o("useCapture defaults to false", function () { + var sequence = [] + var parent = o.spy(function(ev){ + sequence.push("parent") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var target = o.spy(function(ev){ + sequence.push("target") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + $document.body.addEventListener("click", parent) + div.addEventListener("click", target) + div.dispatchEvent(e) + + o(parent.callCount).equals(1) + o(target.callCount).equals(1) + o(sequence).deepEquals(["target", "parent"]) + }) + o("legacy handlers fire on the bubbling phase", function () { + var sequence = [] + var parent = o.spy(function(ev){ + sequence.push("parent") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var target = o.spy(function(ev){ + sequence.push("target") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + $document.body.addEventListener("click", parent) + $document.body.onclick = parent + div.addEventListener("click", target) + div.dispatchEvent(e) + + o(parent.callCount).equals(2) + o(target.callCount).equals(1) + o(sequence).deepEquals(["target", "parent", "parent"]) + }) + o("events do not propagate to child nodes", function() { + var target = o.spy(function(ev){ + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals($document.body) + o(ev.currentTarget).equals($document.body) + }) + var child = o.spy(function(){ + }) + + $document.body.addEventListener("click", target) + div.addEventListener("click", child) + $document.body.dispatchEvent(e) + + o(target.callCount).equals(1) + o(child.callCount).equals(0) + }) + o("e.stopPropagation 1/6", function () { + var capParent = o.spy(function(e){e.stopPropagation()}) + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(0) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 2/6", function () { + var capParent = o.spy() + var capTarget = o.spy(function(e){e.stopPropagation()}) + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + + o("e.stopPropagation 3/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy(function(e){e.stopPropagation()}) + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 4/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy(function(e){e.stopPropagation()}) + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 5/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy(function(e){e.stopPropagation()}) + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("e.stopPropagation 6/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var legacyTarget = o.spy() + var bubTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy(function(e){e.stopPropagation()}) + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("e.stopImmediatePropagation 1/6", function () { + var capParent = o.spy(function(e){e.stopImmediatePropagation()}) + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(0) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 2/6", function () { + var capParent = o.spy() + var capTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + + o("e.stopImmediatePropagation 3/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 4/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 5/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy(function(e){e.stopImmediatePropagation()}) + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 6/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var legacyTarget = o.spy() + var bubTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy(function(e){e.stopImmediatePropagation()}) + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("errors thrown in handlers don't interrupt the chain", function(done) { + var errMsg = "The presence of these six errors in the log is expected in non-Node.js environments" + var handler = o.spy(function(){throw errMsg}) + + $document.body.addEventListener("click", handler, true) + $document.body.addEventListener("click", handler, false) + $document.body.onclick = handler + + div.addEventListener("click", handler, true) + div.addEventListener("click", handler, false) + div.onclick = handler + + div.dispatchEvent(e) + + o(handler.callCount).equals(6) + + // Swallow the async errors in NodeJS + if (typeof process !== "undefined" && typeof process.once === "function"){ + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + done() + }) + }) + }) + }) + }) + }) + } else { + done() + } + }) + }) }) o.spec("attributes", function() { o.spec("a[href]", function() { @@ -731,6 +1257,18 @@ o.spec("domMock", function() { o(input.checked).equals(true) }) + o("doesn't toggle on click when preventDefault() is used", function() { + var input = $document.createElement("input") + input.setAttribute("type", "checkbox") + input.checked = false + input.onclick = function(e) {e.preventDefault()} + + var e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + input.dispatchEvent(e) + + o(input.checked).equals(false) + }) }) o.spec("input[value]", function() { o("only exists in input elements", function() { From 5d1c7d23b95ffb9dfafb0979449359804c6faf24 Mon Sep 17 00:00:00 2001 From: cavemansspa Date: Sun, 3 Dec 2017 15:55:38 -0500 Subject: [PATCH 51/66] docs: Integrating 3rd party libs (#2047) --- docs/integrating-libs.md | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/integrating-libs.md diff --git a/docs/integrating-libs.md b/docs/integrating-libs.md new file mode 100644 index 00000000..42b72bee --- /dev/null +++ b/docs/integrating-libs.md @@ -0,0 +1,57 @@ +# Integrating with Other Libraries +Integration with third party libraries or vanilla javascript code can be achieved via [lifecycle methods](lifecycle-methods.md). + +- [Usage](#usage) + +### Usage +```javascript +var FullCalendar = { + + oncreate: function (vnode) { + console.log('FullCalendar::oncreate') + $(vnode.dom).fullCalendar({ + // put your initial options and callbacks here + }) + + Object.assign(vnode.attrs.parentState, {fullCalendarEl: vnode.dom}) + }, + + // Consider that the lib will modify this parent element in the DOM (e.g. add dependent class attribute and values). + // As long as you return the same view results here, mithril will not + // overwrite the actual DOM because it's always comparing old and new VDOM + // before applying DOM updates. + view: function (vnode) { + return m('div') + }, + + onbeforeremove: function (vnode) { + // Run any destroy / cleanup methods here. + //E.g. $(vnode.state.fullCalendarEl).fullCalendar('destroy') + } +} + +m.mount(document.body, { + view: function (vnode) { + return [ + m('h1', 'Calendar'), + m(FullCalendar, {parentState: vnode.state}), + m('button', {onclick: prev}, 'Mithril Button -'), + m('button', {onclick: next}, 'Mithril Button +') + + ] + + function next() { + $(vnode.state.fullCalendarEl).fullCalendar('next') + } + + function prev() { + $(vnode.state.fullCalendarEl).fullCalendar('prev') + } + + } + +}) + +``` + +Running example [flems: FullCalendar](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHigjQGsACAJxigBeADog4xAJ6w4hGDGKjuhfmBEgSxAA5xEAel3UAJmgBWcfNSi0ArobBQM-C7Sy6MJjAA9d7AEZxdMGsoKGoMWDRDR10AZnwAdnwABkDg0PCmKN58LA4LODhRAD4QAF8KdGxcRAIzShp6RmYagDdHbgAxNIBhDMj2wW5gYTQR4WJ6an4MRkRuILRqYgh6bgAKFrRaQxgASiGxhWI6NDhaWHwrAHM1gHIukN6oft5EREnpxlvdw-GAEg2Wx2+EMLl2+CCjz6WTWw1GR3G+m4mmsxG4EhsvG4HAgy3C3FommW9Dg3AwkW4YRCvgw1E4pNk-F+xFKP1G8PGAHlfCYYEt8BgChArmhAdsYALiMReOZNI4mMQAMrEGYwChDSFQJ6ZRwAUSgc024pBLlZh3KY3hLQgMAA7nMFksVmh1kadvs4eNxvxiNZeC6sHdDBAWt9zRRLeN6L4YGBaPx+FhaC0YA7rItiS6xe6DhziEiAErpsloCTcHbiXi0Mu6SmwcnWTTcHDEQjbBkwJzM-QAt0S8SqiE9aF6qDgzXal5B+DS6th+GlEaL9lYHI2BhrUHUaw4Bj4XzbCTqz3Ea12tMZ52uoF7XNe6XyP0u5DM8aB26EACMt3Vt0nWW+CM8zfNYHi1EdeGPOV+AYZVVUNG98AHRhWSA+8QNuXxUQmNAfzvBEjkmdg6TmTR+BaV8WV-ABZXFlGgbgACFsNWABaQDKPfLCpXoPCT3QnDLAgEjuDQGBPAUYCqO4W5aNbXgGOYniXQAannZkAF1IyOR1M1E8TiDWD1KN7RDkIlCcIP1cdhwiGFbjEiT1KOZdmV0q8yJgFojPw+9TONcyhyhOzRxs4KdV4O5PNDNl71chdLVZMoKhATAcDwfIECoE4mmIPAyg0qh2C4BAUEqdKalyeToHqP1yBqDRtD0XR000TgrmcVwqvoqAAAFP3wAaAFZdG6hSoHwOoqEkTRqhAOpynKuak13PKqDqvBGp0fRWvazrRpcBVeoAJkGgBOfBjoO1bJqykAZrmhaUrSx6AEdrE7CRat4er1ClJqdrQNqOroVwTHez7eriU7P10YNxF0cGPt4CRbvqB68Cepa8E1KkIu+36tua3aQZcVIQjxl4oYSZI4YgBHcYgtHpokWbMYQUoNNKIA) \ No newline at end of file From 4542d2997a60dc09e0b9a065a1d32d99f709d525 Mon Sep 17 00:00:00 2001 From: thepriefy Date: Sun, 3 Dec 2017 16:34:13 +0700 Subject: [PATCH 52/66] issue #1914 docs/support.md should exist --- .github/CONTRIBUTING.md | 3 +++ .github/SUPPORT.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/SUPPORT.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..b553c8d1 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +## Contributing + +There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the mithril site that hopefully helps, but if not definitely hop into the [Gitter Room](https://gitter.im/mithriljs/mithril.js) and ask away! \ No newline at end of file diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..8ed1ebe1 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +## Getting Help + +Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. \ No newline at end of file From 9f1f0f0094bda8eb3c80c955094313ff03b9dd66 Mon Sep 17 00:00:00 2001 From: thepriefy Date: Sun, 3 Dec 2017 16:37:58 +0700 Subject: [PATCH 53/66] delete CONTRIBUTING.md as it is existed in ./docs folder --- .github/CONTRIBUTING.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index b553c8d1..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -## Contributing - -There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the mithril site that hopefully helps, but if not definitely hop into the [Gitter Room](https://gitter.im/mithriljs/mithril.js) and ask away! \ No newline at end of file From 7179692fc4c1c3b991cd69e81c4b1d16a0e38858 Mon Sep 17 00:00:00 2001 From: thepriefy Date: Mon, 4 Dec 2017 06:53:31 +0700 Subject: [PATCH 54/66] change folder of support.md file --- .github/SUPPORT.md => docs/support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/SUPPORT.md => docs/support.md (84%) diff --git a/.github/SUPPORT.md b/docs/support.md similarity index 84% rename from .github/SUPPORT.md rename to docs/support.md index 8ed1ebe1..ae466e16 100644 --- a/.github/SUPPORT.md +++ b/docs/support.md @@ -1,3 +1,3 @@ ## Getting Help -Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. \ No newline at end of file +Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. From 8950760e857ed8c92269f0834f28862adc15eb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 2 Nov 2017 15:51:06 +0100 Subject: [PATCH 55/66] render: extract pool addtition logic, don't run onremove on nodes that move from pool to pool (fix #1990) --- render/render.js | 22 ++++++--- render/tests/test-updateNodes.js | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/render/render.js b/render/render.js index 4fabfc45..ee7cefe1 100644 --- a/render/render.js +++ b/render/render.js @@ -185,7 +185,7 @@ module.exports = function($window) { } } recycling = recycling || isRecyclable(old, vnodes) - if (recycling) { + if (recycling && old.pool != null) { var pool = old.pool old = old.concat(old.pool) } @@ -250,7 +250,14 @@ module.exports = function($window) { if (end < start) break } createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) + removeNodes(old, oldStart, Math.min(oldEnd + 1, pool == null ? old.length : old.length - pool.length), vnodes) + if (pool != null) { + var limit = Math.max(oldStart, old.length - pool.length) + for (; oldEnd >= limit; oldEnd--) { + if (old[oldEnd].skip) old[oldEnd].skip = false + else addToPool(old[oldEnd], vnodes) + } + } } } function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { @@ -455,10 +462,7 @@ module.exports = function($window) { } } removeNodeFromDOM(vnode.dom) - if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements - if (!context.pool) context.pool = [vnode] - else context.pool.push(vnode) - } + addToPool(vnode, context) } } } @@ -467,6 +471,12 @@ module.exports = function($window) { var parent = node.parentNode if (parent != null) parent.removeChild(node) } + function addToPool(vnode, context) { + if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements + if (!context.pool) context.pool = [vnode] + else context.pool.push(vnode) + } + } function onremove(vnode) { if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) if (typeof vnode.tag !== "string") { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index fa5231ea..5f82281b 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -839,6 +839,19 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) + o("onremove doesn't fire from nodes in the pool (#1990)", function () { + var onremove = o.spy() + render(root, [ + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}, + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]} + ]) + render(root, [ + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]} + ]) + render(root,[]) + + o(onremove.callCount).equals(2) + }) o("cached, non-keyed nodes skip diff", function () { var onupdate = o.spy(); var cached = {tag:"a", attrs:{onupdate: onupdate}} @@ -857,6 +870,72 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) + o("keyed cached elements are re-initialized when brought back from the pool", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", key: 1, children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("uneyed cached elements are re-initialized when brought back from the pool", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("keyed cached elements are re-initialized when brought back from nested pools", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", key: 1, children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, [{tag: "div", children: []}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("unkeyed cached elements are re-initialized when brought back from nested pools", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, [{tag: "div", children: []}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + o("null stays in place", function() { var create = o.spy() var update = o.spy() From eaa9f589af62462ae95cbbff3f7db87e2dc9fda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Tue, 21 Nov 2017 21:05:20 +0100 Subject: [PATCH 56/66] render/updateNodes: recycling, clarify terminology, fix logic fix #2003 partim 1 fix #1991 partim 1 --- render/render.js | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/render/render.js b/render/render.js index ee7cefe1..beb84340 100644 --- a/render/render.js +++ b/render/render.js @@ -161,12 +161,13 @@ module.exports = function($window) { } //update - function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { - if (old === vnodes || old == null && vnodes == null) return + function updateNodes(parent, old, vnodes, recyclingParent, hooks, nextSibling, ns) { + if (old === vnodes && !recyclingParent || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) else { - if (old.length === vnodes.length) { + var originalOldLength = old.length, hasPool = false + if (originalOldLength === vnodes.length) { var isUnkeyed = false for (var i = 0; i < vnodes.length; i++) { if (vnodes[i] != null && old[i] != null) { @@ -175,56 +176,57 @@ module.exports = function($window) { } } if (isUnkeyed) { - for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i]) continue + for (var i = 0; i < originalOldLength; i++) { + if (old[i] === vnodes[i] && !recyclingParent || old[i] == null && vnodes[i] == null) continue else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) + else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recyclingParent, ns) } return } } - recycling = recycling || isRecyclable(old, vnodes) - if (recycling && old.pool != null) { - var pool = old.pool + if (isRecyclable(old, vnodes)) { + hasPool = true old = old.concat(old.pool) } - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool while (oldEnd >= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ + o = old[oldStart] + v = vnodes[start] + oFromPool = hasPool && oldStart >= originalOldLength + if (o === v && !oFromPool && !recyclingParent) oldStart++, start++ else if (o == null) oldStart++ else if (v == null) start++ else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) } else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ + o = old[oldEnd] + oFromPool = hasPool && oldEnd >= originalOldLength + if (o === v && !oFromPool && !recyclingParent) oldEnd--, start++ else if (o == null) oldEnd-- else if (v == null) start++ else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) oldEnd--, start++ } else break } } while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- + o = old[oldEnd] + v = vnodes[end] + oFromPool = hasPool && oldEnd >= originalOldLength + if (o === v && !oFromPool && !recyclingParent) oldEnd--, end-- else if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- } @@ -233,12 +235,12 @@ module.exports = function($window) { if (v != null) { var oldIndex = map[v.key] if (oldIndex != null) { - var movable = old[oldIndex] - var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom + o = old[oldIndex] + oFromPool = hasPool && oldIndex >= originalOldLength + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) + insertNode(parent, toFragment(o), nextSibling) + o.skip = true + if (o.dom != null) nextSibling = o.dom } else { var dom = createNode(parent, v, hooks, ns, nextSibling) @@ -250,9 +252,9 @@ module.exports = function($window) { if (end < start) break } createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, Math.min(oldEnd + 1, pool == null ? old.length : old.length - pool.length), vnodes) - if (pool != null) { - var limit = Math.max(oldStart, old.length - pool.length) + removeNodes(old, oldStart, Math.min(oldEnd + 1, originalOldLength), vnodes) + if (hasPool) { + var limit = Math.max(oldStart, originalOldLength) for (; oldEnd >= limit; oldEnd--) { if (old[oldEnd].skip) old[oldEnd].skip = false else addToPool(old[oldEnd], vnodes) From e839c9e80ad0e65d0ec78fcae5e87338606d007f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Tue, 21 Nov 2017 21:08:42 +0100 Subject: [PATCH 57/66] render/updateNodes: Don't fetch the next sibling from the pool --- render/render.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/render/render.js b/render/render.js index beb84340..aa2c3d61 100644 --- a/render/render.js +++ b/render/render.js @@ -178,9 +178,9 @@ module.exports = function($window) { if (isUnkeyed) { for (var i = 0; i < originalOldLength; i++) { if (old[i] === vnodes[i] && !recyclingParent || old[i] == null && vnodes[i] == null) continue - else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) + else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, originalOldLength, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recyclingParent, ns) + else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, originalOldLength, nextSibling), recyclingParent, ns) } return } @@ -200,7 +200,7 @@ module.exports = function($window) { else if (v == null) start++ else if (o.key === v.key) { oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), oFromPool || recyclingParent, ns) + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) } else { @@ -210,8 +210,8 @@ module.exports = function($window) { else if (o == null) oldEnd-- else if (v == null) start++ else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) - if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, originalOldLength, nextSibling)) oldEnd--, start++ } else break @@ -225,7 +225,7 @@ module.exports = function($window) { else if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- @@ -237,7 +237,7 @@ module.exports = function($window) { if (oldIndex != null) { o = old[oldIndex] oFromPool = hasPool && oldIndex >= originalOldLength - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), oFromPool || recyclingParent, ns) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) insertNode(parent, toFragment(o), nextSibling) o.skip = true if (o.dom != null) nextSibling = o.dom @@ -402,8 +402,8 @@ module.exports = function($window) { } else return vnode.dom } - function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { + function getNextSibling(vnodes, i, limit, nextSibling) { + for (; i < limit; i++) { if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom } return nextSibling From 98c053e12b7e0073d0faea485d5e97b448a5d577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Tue, 21 Nov 2017 21:27:12 +0100 Subject: [PATCH 58/66] render/updateNodes: revamp unkeyed list detection, don't skip null nodes in unkeyed lists when old and vnodes don't have the same length Fix #2003 partim 2 --- render/render.js | 64 ++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/render/render.js b/render/render.js index aa2c3d61..1e3f3fef 100644 --- a/render/render.js +++ b/render/render.js @@ -166,44 +166,56 @@ module.exports = function($window) { else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) else { - var originalOldLength = old.length, hasPool = false - if (originalOldLength === vnodes.length) { - var isUnkeyed = false - for (var i = 0; i < vnodes.length; i++) { - if (vnodes[i] != null && old[i] != null) { - isUnkeyed = vnodes[i].key == null && old[i].key == null - break - } + var start = 0, commonLength = Math.min(old.length, vnodes.length), originalOldLength = old.length, hasPool = false, isUnkeyed + for(; start < commonLength; start++) { + if (old[start] != null) { + isUnkeyed = old[start].key == null + break } - if (isUnkeyed) { - for (var i = 0; i < originalOldLength; i++) { - if (old[i] === vnodes[i] && !recyclingParent || old[i] == null && vnodes[i] == null) continue - else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, originalOldLength, nextSibling)) - else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, originalOldLength, nextSibling), recyclingParent, ns) - } - return + if (vnodes[start] != null) { + isUnkeyed = vnodes[start].key == null + break } } + if (isUnkeyed && originalOldLength === vnodes.length) { + // treat it as a tuple, no pool here + for (; start < originalOldLength; start++) { + if (old[start] === vnodes[start] && !recyclingParent || old[start] == null && vnodes[start] == null) continue + else if (old[start] == null) createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, start + 1, originalOldLength, nextSibling)) + else if (vnodes[start] == null) removeNodes(old, start, start + 1, null) + else updateNode(parent, old[start], vnodes[start], hooks, getNextSibling(old, start + 1, originalOldLength, nextSibling), recyclingParent, ns) + } + return + } + if (isRecyclable(old, vnodes)) { hasPool = true old = old.concat(old.pool) } - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool + var oldStart = start, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool + while (oldEnd >= oldStart && end >= start) { o = old[oldStart] v = vnodes[start] oFromPool = hasPool && oldStart >= originalOldLength - if (o === v && !oFromPool && !recyclingParent) oldStart++, start++ - else if (o == null) oldStart++ - else if (v == null) start++ - else if (o.key === v.key) { + if (o === v && !oFromPool && !recyclingParent || o == null && v == null) oldStart++, start++ + else if (o == null) { + if (isUnkeyed) { + createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, ++start, originalOldLength, nextSibling)) + } + oldStart++ + } else if (v == null) { + if (isUnkeyed){ + removeNodes(old, start, start + 1, vnodes) + oldStart++ + } + start++ + } else if (o.key === v.key) { oldStart++, start++ updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { + } else { o = old[oldEnd] oFromPool = hasPool && oldEnd >= originalOldLength if (o === v && !oFromPool && !recyclingParent) oldEnd--, start++ @@ -229,8 +241,7 @@ module.exports = function($window) { if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- - } - else { + } else { if (!map) map = getKeyMap(old, oldEnd) if (v != null) { var oldIndex = map[v.key] @@ -241,8 +252,7 @@ module.exports = function($window) { insertNode(parent, toFragment(o), nextSibling) o.skip = true if (o.dom != null) nextSibling = o.dom - } - else { + } else { var dom = createNode(parent, v, hooks, ns, nextSibling) nextSibling = dom } From 39ff8d721740ffa8ff038f65055de1f1d2160d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 22 Nov 2017 00:05:34 +0100 Subject: [PATCH 59/66] render: make removeNode aware that it is removing children from an object that's brought back from the pool --- render/render.js | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/render/render.js b/render/render.js index 1e3f3fef..61f8f9a6 100644 --- a/render/render.js +++ b/render/render.js @@ -164,7 +164,7 @@ module.exports = function($window) { function updateNodes(parent, old, vnodes, recyclingParent, hooks, nextSibling, ns) { if (old === vnodes && !recyclingParent || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) + else if (vnodes == null) removeNodes(old, 0, old.length, vnodes, recyclingParent) else { var start = 0, commonLength = Math.min(old.length, vnodes.length), originalOldLength = old.length, hasPool = false, isUnkeyed for(; start < commonLength; start++) { @@ -182,7 +182,7 @@ module.exports = function($window) { for (; start < originalOldLength; start++) { if (old[start] === vnodes[start] && !recyclingParent || old[start] == null && vnodes[start] == null) continue else if (old[start] == null) createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, start + 1, originalOldLength, nextSibling)) - else if (vnodes[start] == null) removeNodes(old, start, start + 1, null) + else if (vnodes[start] == null) removeNodes(old, start, start + 1, vnodes, recyclingParent) else updateNode(parent, old[start], vnodes[start], hooks, getNextSibling(old, start + 1, originalOldLength, nextSibling), recyclingParent, ns) } return @@ -207,7 +207,7 @@ module.exports = function($window) { oldStart++ } else if (v == null) { if (isUnkeyed){ - removeNodes(old, start, start + 1, vnodes) + removeNodes(old, start, start + 1, vnodes, recyclingParent) oldStart++ } start++ @@ -262,7 +262,7 @@ module.exports = function($window) { if (end < start) break } createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, Math.min(oldEnd + 1, originalOldLength), vnodes) + removeNodes(old, oldStart, Math.min(oldEnd + 1, originalOldLength), vnodes, recyclingParent) if (hasPool) { var limit = Math.max(oldStart, originalOldLength) for (; oldEnd >= limit; oldEnd--) { @@ -296,7 +296,7 @@ module.exports = function($window) { else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) } else { - removeNode(old, null) + removeNode(old, null, recycling) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -368,7 +368,7 @@ module.exports = function($window) { vnode.domSize = vnode.instance.domSize } else if (old.instance != null) { - removeNode(old.instance, null) + removeNode(old.instance, null, recycling) vnode.dom = undefined vnode.domSize = 0 } @@ -434,37 +434,41 @@ module.exports = function($window) { } //remove - function removeNodes(vnodes, start, end, context) { + function removeNodes(vnodes, start, end, context, recycling) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { if (vnode.skip) vnode.skip = false - else removeNode(vnode, context) + else removeNode(vnode, context, recycling) } } } - function removeNode(vnode, context) { + function removeNode(vnode, context, recycling) { var expected = 1, called = 0 var original = vnode.state - if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode.attrs.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - expected++ - result.then(continuation, continuation) + if (!recycling) { + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } - } - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { - var result = callHook.call(vnode.state.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - expected++ - result.then(continuation, continuation) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } } continuation() function continuation() { if (++called === expected) { - checkState(vnode, original) - onremove(vnode) + if (!recycling) { + checkState(vnode, original) + onremove(vnode) + } if (vnode.dom) { var count = vnode.domSize || 1 if (count > 1) { From 9c835f4eac43280c48e267cdef8c290781d42fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Wed, 22 Nov 2017 00:19:35 +0100 Subject: [PATCH 60/66] render/updateNodes: call toFragment on the new vnodes, solves issues with actual fragments (fix #1991 partim 2) --- render/render.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/render/render.js b/render/render.js index 61f8f9a6..541bfd55 100644 --- a/render/render.js +++ b/render/render.js @@ -214,7 +214,7 @@ module.exports = function($window) { } else if (o.key === v.key) { oldStart++, start++ updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) - if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(v), nextSibling) } else { o = old[oldEnd] oFromPool = hasPool && oldEnd >= originalOldLength @@ -223,7 +223,7 @@ module.exports = function($window) { else if (v == null) start++ else if (o.key === v.key) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) - if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, originalOldLength, nextSibling)) + if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(v), getNextSibling(old, oldStart, originalOldLength, nextSibling)) oldEnd--, start++ } else break @@ -238,7 +238,7 @@ module.exports = function($window) { else if (v == null) end-- else if (o.key === v.key) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) - if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(v), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- } else { @@ -249,7 +249,7 @@ module.exports = function($window) { o = old[oldIndex] oFromPool = hasPool && oldIndex >= originalOldLength updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) - insertNode(parent, toFragment(o), nextSibling) + insertNode(parent, toFragment(v), nextSibling) o.skip = true if (o.dom != null) nextSibling = o.dom } else { From fc0240da0ff2860e1364f304243a83e732e386ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 23 Nov 2017 00:24:18 +0100 Subject: [PATCH 61/66] Tests for #1990, #1991 and #2003 --- render/tests/test-onremove.js | 27 +++++++++++++- render/tests/test-updateNodes.js | 64 ++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index f509e704..cc242a25 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -101,6 +101,7 @@ o.spec("onremove", function() { render(root, temp) render(root, updated) + o(vnodes[0].dom).equals(updated[0].dom) o(remove.callCount).equals(1) }) o("does not recycle when there's an onremove", function() { @@ -132,7 +133,7 @@ o.spec("onremove", function() { }) render(root, {tag: comp}) render(root, null) - + o(spy.callCount).equals(1) }) o("calls onremove on nested component child", function() { @@ -148,7 +149,7 @@ o.spec("onremove", function() { }) render(root, {tag: comp}) render(root, null) - + o(spy.callCount).equals(1) }) o("doesn't call onremove on children when the corresponding view returns null (after removing the parent)", function() { @@ -191,6 +192,28 @@ o.spec("onremove", function() { o(spy.callCount).equals(0) o(threw).equals(false) }) + o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() { + var onremove = o.spy(); + + render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]); + render(root, [m("div", m("div"))]); + render(root, []); + + o(onremove.callCount).equals(1) + }) + o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() { + var onremove = o.spy() + var vnode = {tag: "div", key: 1, children: [{tag: "div", attrs: {onremove: onremove}}]} + var temp = {tag: "div", key: 2} + var updated = {tag: "div", key: 1, children: [{tag: "p"}]} + + render(root, [vnode]) + render(root, [temp]) + render(root, [updated]) + + o(vnode.dom).equals(updated.dom) + o(onremove.callCount).equals(1) + }) }) }) }) \ No newline at end of file diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 5f82281b..65d81916 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -870,7 +870,7 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) - o("keyed cached elements are re-initialized when brought back from the pool", function () { + o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() var cached = { @@ -886,7 +886,7 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) - o("uneyed cached elements are re-initialized when brought back from the pool", function () { + o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() var cached = { @@ -902,7 +902,7 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) - o("keyed cached elements are re-initialized when brought back from nested pools", function () { + o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() var cached = { @@ -919,7 +919,7 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) - o("unkeyed cached elements are re-initialized when brought back from nested pools", function () { + o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { var onupdate = o.spy() var oncreate = o.spy() var cached = { @@ -983,6 +983,62 @@ o.spec("updateNodes", function() { o(vnode.dom).notEquals(updated.dom) }) + o("don't add back elements from fragments that are restored from the pool #1991", function() { + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: [{tag: "div"}]} + ]) + render(root, [ + {tag: "[", children: [null]} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + + o(root.childNodes.length).equals(0) + }) + o("don't add back elements from fragments that are being removed #1991", function() { + render(root, [ + {tag: "[", children: []}, + {tag: "p"}, + ]) + render(root, [ + {tag: "[", children: [{tag: "div", text: 5}]} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + + o(root.childNodes.length).equals(0) + }) + o("handles null values in unkeyed lists of different length (#2003)", function() { + var oncreate = o.spy(); + var onremove = o.spy(); + var onupdate = o.spy(); + function attrs() { + return {oncreate: oncreate, onremove: onremove, onupdate: onupdate} + } + + render(root, [{tag: "div", attrs: attrs()}, null]); + render(root, [null, {tag: "div", attrs: attrs()}, null]); + + o(oncreate.callCount).equals(2) + o(onremove.callCount).equals(1) + o(onupdate.callCount).equals(0) + }) + o("don't fetch the nextSibling from the pool", function() { + render(root, [{tag: "[", children: [{tag: "div", key: 1}, {tag: "div", key: 2}]}, {tag: "p"}]) + render(root, [{tag: "[", children: []}, {tag: "p"}]) + render(root, [{tag: "[", children: [{tag: "div", key: 2}, {tag: "div", key: 1}]}, {tag: "p"}]) + + o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) + }) components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create From 02aab654f0b1b02e35234fca9dc47c306ba62bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Thu, 23 Nov 2017 15:06:39 +0100 Subject: [PATCH 62/66] render: remove check that may hide bugs --- render/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render/render.js b/render/render.js index 541bfd55..3936e5ed 100644 --- a/render/render.js +++ b/render/render.js @@ -420,7 +420,7 @@ module.exports = function($window) { } function insertNode(parent, dom, nextSibling) { - if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling) + if (nextSibling) parent.insertBefore(dom, nextSibling) else parent.appendChild(dom) } From 3f37d3d7c06885ad811a6a4307859101d0bd5328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Fri, 24 Nov 2017 00:29:15 +0100 Subject: [PATCH 63/66] #2021 change log and docs --- docs/change-log.md | 1 + render/render.js | 125 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index a7d3b220..25e0a231 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -42,6 +42,7 @@ - core: `xlink:href` attributes are now correctly removed - render: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992) - render: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) +- render: fix various updateNodes/removeNodes issues when the pool and fragments are involved [#1990](https://github.com/MithrilJS/mithril.js/issues/1990), [#1991](https://github.com/MithrilJS/mithril.js/issues/1991), [#2003](https://github.com/MithrilJS/mithril.js/issues/2003), [#2021](https://github.com/MithrilJS/mithril.js/pull/2021) --- diff --git a/render/render.js b/render/render.js index 3936e5ed..de3d8905 100644 --- a/render/render.js +++ b/render/render.js @@ -161,6 +161,124 @@ module.exports = function($window) { } //update + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {boolean} recyclingParent - was the parent vnode or one of its ancestor + * fetched from the recycling pool? + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next DOM node if we're dealing with a + * fragment that is not the last item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. describe how the recycling pool meshes into this + // 4. discuss DOM node operations. + + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed + // (we take advantage of the fact that mixed diff is not supported and settle on the + // keyedness of the first vnode we find) + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // - nodes left in the recycling pool? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + + // ## Diffing + // + // There's first a simple diff for unkeyed lists of equal length that eschews the pool. + // + // It is followed by a small section that activates the recycling pool if present, we'll + // ignore it for now. + // + // Then comes the main diff algorithm that is split in four parts (simplifying a bit). + // + // The first part goes through both lists top-down as long as the nodes at each level have + // the same key. This is always true for unkeyed lists that are entirely processed by this + // step. + // + // The second part deals with lists reversals, and traverses one list top-down and the other + // bottom-up (as long as the keys match). + // + // The third part goes through both lists bottom up as long as the keys match. + // + // The first and third sections allow us to deal efficiently with situations where one or + // more contiguous nodes were either inserted into, removed from or re-ordered in an otherwise + // sorted list. They may reduce the number of nodes to be processed in the fourth section. + // + // The fourth section does keyed diff for the situations not covered by the other three. It + // builds a {key: oldIndex} dictionary and uses it to find old nodes that match the keys of + // new ones. + // The nodes from the `old` array that have a match in the new `vnodes` one are marked as + // `vnode.skip: true`. + // + // If there are still nodes in the new `vnodes` array that haven't been matched to old ones, + // they are created. + // The range of old nodes that wasn't covered by the first three sections is passed to + // `removeNodes()`. Those nodes are removed unless marked as `.skip: true`. + // + // Then some pool business happens. + // + // It should be noted that the description of the four sections above is not perfect, because those + // parts are actually implemented as only two loops, one for the first two parts, and one for + // the other two. I'm not sure it wins us anything except maybe a few bytes of file size. + + // ## The pool + // + // `old.pool` is an optional array that holds the vnodes that have been previously removed + // from the DOM at this level (provided they met the pool eligibility criteria). + // + // If the `old`, `old.pool` and `vnodes` meet some criteria described in `isRecyclable`, the + // elements of the pool are appended to the `old` array, which enables the reconciler to find + // them. + // + // While this is optimal for unkeyed diff and map-based keyed diff (the fourth diff part), + // that strategy clashes with the second and third parts of the main diff algo, because + // the end of the old list is now filled with the nodes of the pool. + // + // To determine if a vnode was brought back from the pool, we look at its position in the + // `old` array (see the various `oFromPool` definitions). That information is important + // in three circumstances: + // - If the old and the new vnodes are the same object (`===`), diff is not performed unless + // the old node comes from the pool (since it must be recycled/re-created). + // - The value of `oFromPool` is passed as the `recycling` parameter of `upateNode()` (whether + // the parent is being recycled is also factred in here) + // - It is used in the DOM node insertion logic (see below) + // + // At the very end of `updateNodes()`, the nodes in the pool that haven't been picked back + // are put in the new pool for the next render phase. + // + // The pool eligibility and `isRecyclable()` criteria are to be updated as part of #1675. + + // ## DOM node operations + // + // In most cases `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo), or + // if the node was brough back from the pool and both the old and new nodes have the same + // `.tag` value (when the `.tag` differ, `updateNode()` does the insertion). + // + // The fourth part of the diff currently inserts nodes unconditionally, leading to issues + // like #1791 and #1999. We need to be smarter about those situations where adjascent old + // nodes remain together in the new list in a way that isn't covered by parts one and + // three of the diff algo. + function updateNodes(parent, old, vnodes, recyclingParent, hooks, nextSibling, ns) { if (old === vnodes && !recyclingParent || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) @@ -178,7 +296,6 @@ module.exports = function($window) { } } if (isUnkeyed && originalOldLength === vnodes.length) { - // treat it as a tuple, no pool here for (; start < originalOldLength; start++) { if (old[start] === vnodes[start] && !recyclingParent || old[start] == null && vnodes[start] == null) continue else if (old[start] == null) createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, start + 1, originalOldLength, nextSibling)) @@ -272,6 +389,8 @@ module.exports = function($window) { } } } + // when recycling, we're re-using an old DOM node, but firing the oninit/oncreate hooks + // instead of onbeforeupdate/onupdate. function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { @@ -412,6 +531,8 @@ module.exports = function($window) { } else return vnode.dom } + // the vnodes array may hold items that come from the pool (after `limit`) they should + // be ignored function getNextSibling(vnodes, i, limit, nextSibling) { for (; i < limit; i++) { if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom @@ -443,6 +564,8 @@ module.exports = function($window) { } } } + // when a node is removed from a parent that's brought back from the pool, its hooks should + // not fire. function removeNode(vnode, context, recycling) { var expected = 1, called = 0 var original = vnode.state From 9f09ac069c649560ebe71b8368b03c1dbc82c4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20G=C3=A9rardy?= Date: Mon, 4 Dec 2017 14:33:21 +0100 Subject: [PATCH 64/66] Address #2021 review comments --- render/render.js | 27 ++++++++++++--------------- render/tests/test-onremove.js | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/render/render.js b/render/render.js index de3d8905..a1478384 100644 --- a/render/render.js +++ b/render/render.js @@ -189,8 +189,9 @@ module.exports = function($window) { // The updateNodes() function: // - deals with trivial cases // - determines whether the lists are keyed or unkeyed - // (we take advantage of the fact that mixed diff is not supported and settle on the - // keyedness of the first vnode we find) + // (Currently we look for the first pair of non-null nodes and deem the lists unkeyed + // if both nodes are unkeyed. TODO (v2) We may later take advantage of the fact that + // mixed diff is not supported and settle on the keyedness of the first vnode we find) // - diffs them and patches the DOM if needed (that's the brunt of the code) // - manages the leftovers: after diffing, are there: // - old nodes left to remove? @@ -258,7 +259,7 @@ module.exports = function($window) { // in three circumstances: // - If the old and the new vnodes are the same object (`===`), diff is not performed unless // the old node comes from the pool (since it must be recycled/re-created). - // - The value of `oFromPool` is passed as the `recycling` parameter of `upateNode()` (whether + // - The value of `oFromPool` is passed as the `recycling` parameter of `updateNode()` (whether // the parent is being recycled is also factred in here) // - It is used in the DOM node insertion logic (see below) // @@ -284,19 +285,15 @@ module.exports = function($window) { else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes, recyclingParent) else { - var start = 0, commonLength = Math.min(old.length, vnodes.length), originalOldLength = old.length, hasPool = false, isUnkeyed + var start = 0, commonLength = Math.min(old.length, vnodes.length), originalOldLength = old.length, hasPool = false, isUnkeyed = false for(; start < commonLength; start++) { - if (old[start] != null) { - isUnkeyed = old[start].key == null - break - } - if (vnodes[start] != null) { - isUnkeyed = vnodes[start].key == null + if (old[start] != null && vnodes[start] != null) { + if (old[start].key == null && vnodes[start].key == null) isUnkeyed = true break } } if (isUnkeyed && originalOldLength === vnodes.length) { - for (; start < originalOldLength; start++) { + for (start = 0; start < originalOldLength; start++) { if (old[start] === vnodes[start] && !recyclingParent || old[start] == null && vnodes[start] == null) continue else if (old[start] == null) createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, start + 1, originalOldLength, nextSibling)) else if (vnodes[start] == null) removeNodes(old, start, start + 1, vnodes, recyclingParent) @@ -310,7 +307,7 @@ module.exports = function($window) { old = old.concat(old.pool) } - var oldStart = start, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool + var oldStart = start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool while (oldEnd >= oldStart && end >= start) { o = old[oldStart] @@ -318,12 +315,12 @@ module.exports = function($window) { oFromPool = hasPool && oldStart >= originalOldLength if (o === v && !oFromPool && !recyclingParent || o == null && v == null) oldStart++, start++ else if (o == null) { - if (isUnkeyed) { + if (isUnkeyed || v.key == null) { createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, ++start, originalOldLength, nextSibling)) } oldStart++ } else if (v == null) { - if (isUnkeyed){ + if (isUnkeyed || o.key == null) { removeNodes(old, start, start + 1, vnodes, recyclingParent) oldStart++ } @@ -568,8 +565,8 @@ module.exports = function($window) { // not fire. function removeNode(vnode, context, recycling) { var expected = 1, called = 0 - var original = vnode.state if (!recycling) { + var original = vnode.state if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { var result = callHook.call(vnode.attrs.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index cc242a25..5ab20caf 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -195,7 +195,7 @@ o.spec("onremove", function() { o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() { var onremove = o.spy(); - render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]); + render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]); render(root, [m("div", m("div"))]); render(root, []); From b88e86f6f0cc16dee52d8949e2dc4ec626bdc42f Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Mon, 4 Dec 2017 13:40:41 +0000 Subject: [PATCH 65/66] Bundled output for commit 9f09ac069c649560ebe71b8368b03c1dbc82c4aa [skip ci] --- README.md | 2 +- mithril.js | 299 ++++++++++++++++++++++++++++++++++++------------- mithril.min.js | 92 +++++++-------- 3 files changed, 266 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index b9240f22..e797acf0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww ## What is Mithril? -A modern client-side Javascript framework for building Single Page Applications. It's small (8.48 KB gzipped), fast and provides routing and XHR utilities out of the box. +A modern client-side Javascript framework for building Single Page Applications. It's small (8.59 KB gzipped), fast and provides routing and XHR utilities out of the box. Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. diff --git a/mithril.js b/mithril.js index 182ca224..8546f584 100644 --- a/mithril.js +++ b/mithril.js @@ -533,85 +533,204 @@ var coreRenderer = function($window) { } } //update - function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { - if (old === vnodes || old == null && vnodes == null) return + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last0 `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {boolean} recyclingParent - was the parent vnode or one of its ancestor + * fetched from the recycling pool? + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next0 DOM node if we're dealing with a + * fragment that is not the last0 item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. describe how the recycling pool meshes into this + // 4. discuss DOM node operations. + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed + // (Currently we look for the first pair of non-null nodes and deem the lists unkeyed + // if both nodes are unkeyed. TODO (v2) We may later take advantage of the fact that + // mixed diff is not supported and settle on the keyedness of the first vnode we find) + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // - nodes left in the recycling pool? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + // ## Diffing + // + // There's first a simple diff for unkeyed lists of equal length that eschews the pool. + // + // It is followed by a small section that activates the recycling pool if present, we'll + // ignore it for now. + // + // Then comes the main diff algorithm that is split in four parts (simplifying a bit). + // + // The first part goes through both lists top-down as long as the nodes at each level have + // the same key2. This is always true for unkeyed lists that are entirely processed by this + // step. + // + // The second part deals with lists reversals, and traverses one list top-down and the other + // bottom-up (as long as the keys match1). + // + // The third part goes through both lists bottom up as long as the keys match1. + // + // The first and third sections allow us to deal efficiently with situations where one or + // more contiguous nodes were either inserted into, removed from or re-ordered in an otherwise + // sorted list. They may reduce the number of nodes to be processed in the fourth section. + // + // The fourth section does keyed diff for the situations not covered by the other three. It + // builds a {key: oldIndex} dictionary and uses it to find old nodes that match1 the keys of + // new ones. + // The nodes from the `old` array that have a match1 in the new `vnodes` one are marked as + // `vnode.skip: true`. + // + // If there are still nodes in the new `vnodes` array that haven't been matched to old ones, + // they are created. + // The range of old nodes that wasn't covered by the first three sections is passed to + // `removeNodes()`. Those nodes are removed unless marked as `.skip: true`. + // + // Then some pool business happens. + // + // It should be noted that the description of the four sections above is not perfect, because those + // parts are actually implemented as only two loops, one for the first two parts, and one for + // the other two. I'm1 not sure it wins us anything except maybe a few bytes of file size. + // ## The pool + // + // `old.pool` is an optional array that holds the vnodes that have been previously removed + // from the DOM at this level (provided they met the pool eligibility criteria). + // + // If the `old`, `old.pool` and `vnodes` meet some criteria described in `isRecyclable`, the + // elements of the pool are appended to the `old` array, which enables the reconciler to find + // them. + // + // While this is optimal for unkeyed diff and map-based keyed diff (the fourth diff part), + // that strategy clashes with the second and third parts of the main diff algo, because + // the end of the old list is now filled with the nodes of the pool. + // + // To determine if a vnode was brought back from the pool, we look at its position in the + // `old` array (see the various `oFromPool` definitions). That information is important + // in three circumstances: + // - If the old and the new vnodes are the same object (`===`), diff is not performed unless + // the old node comes from the pool (since it must be recycled/re-created). + // - The value of `oFromPool` is passed as the `recycling` parameter of `updateNode()` (whether + // the parent is being recycled is also factred in here) + // - It is used in the DOM node insertion logic (see below) + // + // At the very end of `updateNodes()`, the nodes in the pool that haven't been picked back + // are put in the new pool for the next0 render phase. + // + // The pool eligibility and `isRecyclable()` criteria are to be updated as part of #1675. + // ## DOM node operations + // + // In most cases `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo), or + // if the node was brough back from the pool and both the old and new nodes have the same + // `.tag` value (when the `.tag` differ, `updateNode()` does the insertion). + // + // The fourth part of the diff currently inserts nodes unconditionally, leading to issues + // like #1791 and #1999. We need to be smarter about those situations where adjascent old + // nodes remain together in the new list in a way that isn't covered by parts one and + // three of the diff algo. + function updateNodes(parent, old, vnodes, recyclingParent, hooks, nextSibling, ns) { + if (old === vnodes && !recyclingParent || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) - else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) + else if (vnodes == null) removeNodes(old, 0, old.length, vnodes, recyclingParent) else { - if (old.length === vnodes.length) { - var isUnkeyed = false - for (var i = 0; i < vnodes.length; i++) { - if (vnodes[i] != null && old[i] != null) { - isUnkeyed = vnodes[i].key == null && old[i].key == null - break - } - } - if (isUnkeyed) { - for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) - else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) - } - return + var start = 0, commonLength = Math.min(old.length, vnodes.length), originalOldLength = old.length, hasPool = false, isUnkeyed = false + for(; start < commonLength; start++) { + if (old[start] != null && vnodes[start] != null) { + if (old[start].key == null && vnodes[start].key == null) isUnkeyed = true + break } } - recycling = recycling || isRecyclable(old, vnodes) - if (recycling) { - var pool = old.pool + if (isUnkeyed && originalOldLength === vnodes.length) { + for (start = 0; start < originalOldLength; start++) { + if (old[start] === vnodes[start] && !recyclingParent || old[start] == null && vnodes[start] == null) continue + else if (old[start] == null) createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, start + 1, originalOldLength, nextSibling)) + else if (vnodes[start] == null) removeNodes(old, start, start + 1, vnodes, recyclingParent) + else updateNode(parent, old[start], vnodes[start], hooks, getNextSibling(old, start + 1, originalOldLength, nextSibling), recyclingParent, ns) + } + return + } + if (isRecyclable(old, vnodes)) { + hasPool = true old = old.concat(old.pool) } - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + var oldStart = start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oFromPool while (oldEnd >= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ - else if (o == null) oldStart++ - else if (v == null) start++ - else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) + o = old[oldStart] + v = vnodes[start] + oFromPool = hasPool && oldStart >= originalOldLength + if (o === v && !oFromPool && !recyclingParent || o == null && v == null) oldStart++, start++ + else if (o == null) { + if (isUnkeyed || v.key == null) { + createNode(parent, vnodes[start], hooks, ns, getNextSibling(old, ++start, originalOldLength, nextSibling)) + } + oldStart++ + } else if (v == null) { + if (isUnkeyed || o.key == null) { + removeNodes(old, start, start + 1, vnodes, recyclingParent) + oldStart++ + } + start++ + } else if (o.key === v.key) { oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(v), nextSibling) + } else { + o = old[oldEnd] + oFromPool = hasPool && oldEnd >= originalOldLength + if (o === v && !oFromPool && !recyclingParent) oldEnd--, start++ else if (o == null) oldEnd-- else if (v == null) start++ else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag || start < end) insertNode(parent, toFragment(v), getNextSibling(old, oldStart, originalOldLength, nextSibling)) oldEnd--, start++ } else break } } while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- + o = old[oldEnd] + v = vnodes[end] + oFromPool = hasPool && oldEnd >= originalOldLength + if (o === v && !oFromPool && !recyclingParent) oldEnd--, end-- else if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) + if (oFromPool && o.tag === v.tag) insertNode(parent, toFragment(v), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- - } - else { + } else { if (!map) map = getKeyMap(old, oldEnd) if (v != null) { var oldIndex = map[v.key] if (oldIndex != null) { - var movable = old[oldIndex] - var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom - } - else { + o = old[oldIndex] + oFromPool = hasPool && oldIndex >= originalOldLength + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, originalOldLength, nextSibling), oFromPool || recyclingParent, ns) + insertNode(parent, toFragment(v), nextSibling) + o.skip = true + if (o.dom != null) nextSibling = o.dom + } else { var dom = createNode(parent, v, hooks, ns, nextSibling) nextSibling = dom } @@ -621,9 +740,18 @@ var coreRenderer = function($window) { if (end < start) break } createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) + removeNodes(old, oldStart, Math.min(oldEnd + 1, originalOldLength), vnodes, recyclingParent) + if (hasPool) { + var limit = Math.max(oldStart, originalOldLength) + for (; oldEnd >= limit; oldEnd--) { + if (old[oldEnd].skip) old[oldEnd].skip = false + else addToPool(old[oldEnd], vnodes) + } + } } } + // when recycling, we're re-using an old DOM node, but firing the oninit/oncreate hooks + // instead of onbeforeupdate/onupdate. function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { @@ -648,7 +776,7 @@ var coreRenderer = function($window) { else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) } else { - removeNode(old, null) + removeNode(old, null, recycling) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -719,7 +847,7 @@ var coreRenderer = function($window) { vnode.domSize = vnode.instance.domSize } else if (old.instance != null) { - removeNode(old.instance, null) + removeNode(old.instance, null, recycling) vnode.dom = undefined vnode.domSize = 0 } @@ -763,14 +891,16 @@ var coreRenderer = function($window) { } else return vnode.dom } - function getNextSibling(vnodes, i, nextSibling) { - for (; i < vnodes.length; i++) { + // the vnodes array may hold items that come from the pool (after `limit`) they should + // be ignored + function getNextSibling(vnodes, i, limit, nextSibling) { + for (; i < limit; i++) { if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom } return nextSibling } function insertNode(parent, dom, nextSibling) { - if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling) + if (nextSibling) parent.insertBefore(dom, nextSibling) else parent.appendChild(dom) } function setContentEditable(vnode) { @@ -782,37 +912,43 @@ var coreRenderer = function($window) { else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted") } //remove - function removeNodes(vnodes, start, end, context) { + function removeNodes(vnodes, start, end, context, recycling) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { if (vnode.skip) vnode.skip = false - else removeNode(vnode, context) + else removeNode(vnode, context, recycling) } } } - function removeNode(vnode, context) { + // when a node is removed from a parent that's brought back from the pool, its hooks should + // not fire. + function removeNode(vnode, context, recycling) { var expected = 1, called = 0 - var original = vnode.state - if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = callHook.call(vnode.attrs.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - expected++ - result.then(continuation, continuation) + if (!recycling) { + var original = vnode.state + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } - } - if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { - var result = callHook.call(vnode.state.onbeforeremove, vnode) - if (result != null && typeof result.then === "function") { - expected++ - result.then(continuation, continuation) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) + if (result != null && typeof result.then === "function") { + expected++ + result.then(continuation, continuation) + } } } continuation() function continuation() { if (++called === expected) { - checkState(vnode, original) - onremove(vnode) + if (!recycling) { + checkState(vnode, original) + onremove(vnode) + } if (vnode.dom) { var count0 = vnode.domSize || 1 if (count0 > 1) { @@ -822,10 +958,7 @@ var coreRenderer = function($window) { } } removeNodeFromDOM(vnode.dom) - if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements - if (!context.pool) context.pool = [vnode] - else context.pool.push(vnode) - } + addToPool(vnode, context) } } } @@ -834,6 +967,12 @@ var coreRenderer = function($window) { var parent = node.parentNode if (parent != null) parent.removeChild(node) } + function addToPool(vnode, context) { + if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements + if (!context.pool) context.pool = [vnode] + else context.pool.push(vnode) + } + } function onremove(vnode) { if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) if (typeof vnode.tag !== "string") { diff --git a/mithril.min.js b/mithril.min.js index 8c155377..3a305535 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,46 +1,46 @@ -(function(){function x(a,c,e,f,m,l){return{tag:a,key:c,attrs:e,children:f,text:m,dom:l,domSize:void 0,state:void 0,events:void 0,instance:void 0,skip:!1}}function N(a){for(var c in a)if(C.call(a,c))return!1;return!0}function A(a){var c=arguments[1],e=2;if(null==a||"string"!==typeof a&&"function"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a){var f;if(!(f=O[a])){var m="div";for(var l=[],g={};f=S.exec(a);){var n=f[1], -r=f[2];""===n&&""!==r?m=r:"#"===n?g.id=r:"."===n?l.push(r):"["===f[3][0]&&((n=f[6])&&(n=n.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===f[4]?l.push(n):g[f[4]]=""===n?n:n||!0)}0b.indexOf("?")?"?":"&";b+=e+c}return b}function g(b){try{return""!==b?JSON.parse(b):null}catch(v){throw Error(b);}}function n(b){return b.responseText}function r(b,a){if("function"===typeof b)if(Array.isArray(a))for(var c=0;ck.status||304===k.status||W.test(b.url))c(r(b.type,a));else{var f=Error(k.responseText);f.code=k.status;f.response=a;e(f)}}catch(X){e(X)}};f&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?v:y(v)},jsonp:function(b,g){var y=e();b=f(b, -g);var n=new c(function(c,e){var f=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,g=a.document.createElement("script");a[f]=function(e){g.parentNode.removeChild(g);c(r(b.type,e));delete a[f]};g.onerror=function(){g.parentNode.removeChild(g);e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=m(b.url,b.data);b.data[b.callbackKey||"callback"]=f;g.src=l(b.url,b.data);a.document.documentElement.appendChild(g)});return!0===b.background?n:y(n)},setCompletionCallback:function(b){z= -b}}}(window,p),R=function(a){function c(h,d){if(h.state!==d)throw Error("`vnode.state` must not be modified");}function e(h){var d=h.state;try{return this.apply(d,arguments)}finally{c(h,d)}}function f(h,d,b,a,c,e,f){for(;b=u&&D>=q;){var w=d[u],p=a[q];if(w!==p||c)if(null==w)u++;else if(null==p)q++;else if(w.key===p.key){var t=null!=n&&u>=d.length-n.length||null==n&&c;u++;q++;r(h,w,p,e,z(d,u,g),t,l);c&&w.tag===p.tag&&b(h,k(w),g)}else if(w=d[v],w!==p||c)if(null==w)v--;else if(null==p)q++;else if(w.key===p.key)t=null!=n&&v>=d.length-n.length||null==n&&c,r(h,w,p,e,z(d,v+1,g),t,l),(c||q=u&&D>=q;){w=d[v];p=a[D];if(w!==p||c)if(null==w)v--;else{if(null!=p)if(w.key===p.key)t=null!=n&&v>=d.length-n.length||null==n&&c,r(h,w,p,e,z(d,v+1,g),t,l),c&&w.tag===p.tag&&b(h,k(w),g),null!=w.dom&&(g=w.dom),v--;else{if(!H){H=d;t=v;w={};var B;for(B=0;B=d.length-n.length||null==n&&c,r(h,B,p,e,z(d,v+1,g),t,l),b(h,k(B),g),d[w].skip=!0,null!=B.dom&&(g=B.dom)):g=m(h,p,e, -l,g))}D--}else v--,D--;if(Dc.indexOf("?")?"?":"&";c+=e+d}return c}function k(c){try{return""!==c?JSON.parse(c):null}catch(C){throw Error(c);}}function q(c){return c.responseText}function r(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;dm.status||304===m.status||Z.test(c.url))d(r(c.type,a));else{var h=Error(m.responseText);h.code=m.status;h.response=a;e(h)}}catch(H){e(H)}};h&&null!=c.data?m.send(c.data):m.send()});return!0===c.background?C:A(C)},jsonp:function(c,k){var A=e();c=h(c, +k);var q=new d(function(d,e){var h=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[h]=function(e){k.parentNode.removeChild(k);d(r(c.type,e));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);e(Error("JSONP request failed"));delete a[h]};null==c.data&&(c.data={});c.url=p(c.url,c.data);c.data[c.callbackKey||"callback"]=h;k.src=l(c.url,c.data);a.document.documentElement.appendChild(k)});return!0===c.background?q:A(q)},setCompletionCallback:function(c){u= +c}}}(window,B),U=function(a){function d(g,b){if(g.state!==b)throw Error("`vnode.state` must not be modified");}function e(g){var b=g.state;try{return this.apply(b,arguments)}finally{d(g,b)}}function h(g,b,f,c,a,d,e){for(;f=v&&t>=n;)if(w=b[v],z=f[n],y=C&&v>=q,w===z&&!y&&!a||null==w&&null==z)v++,n++;else if(null==w)(E||null==z.key)&&p(g,f[n],d,k,u(b,++n,q,e)),v++;else if(null==z){if(E||null==w.key)A(b,n,n+1,f,a),v++;n++}else if(w.key===z.key)v++,n++,r(g,w,z,d,u(b,v,q,e),y||a,k),y&&w.tag===z.tag&&c(g,m(z),e);else if(w=b[l],y=C&&l>=q,w!==z||y||a)if(null==w)l--;else if(null== +z)n++;else if(w.key===z.key)r(g,w,z,d,u(b,l+1,q,e),y||a,k),(y&&w.tag===z.tag||n=v&&t>=n;){w=b[l];z=f[t];y=C&&l>=q;if(w!==z||y||a)if(null==w)l--;else{if(null!=z)if(w.key===z.key)r(g,w,z,d,u(b,l+1,q,e),y||a,k),y&&w.tag===z.tag&&c(g,m(z),e),null!=w.dom&&(e=w.dom),l--;else{if(!I){I=b;E=l;w={};for(y=0;y=q,r(g,w,z,d,u(b,l+1,q,e),y||a,k), +c(g,m(z),e),w.skip=!0,null!=w.dom&&(e=w.dom)):e=p(g,z,d,k,e))}t--}else l--,t--;if(t=g;l--)b[l].skip?b[l].skip=!1:B(b[l],f)}}}function r(g,b,f,a,c,d,h){var n=b.tag;if(n===f.tag){f.state=b.state;f.events=b.events;var A;if(A=!d){var u,E;null!=f.attrs&&"function"===typeof f.attrs.onbeforeupdate&&(u=e.call(f.attrs.onbeforeupdate,f,b));"string"!==typeof f.tag&&"function"===typeof f.state.onbeforeupdate&&(E=e.call(f.state.onbeforeupdate, +f,b));void 0===u&&void 0===E||u||E?A=!1:(f.dom=b.dom,f.domSize=b.domSize,f.instance=b.instance,A=!0)}if(!A)if("string"===typeof n)switch(null!=f.attrs&&(d?(f.state={},K(f.attrs,f,a)):M(f.attrs,f,a)),n){case "#":b.children.toString()!==f.children.toString()&&(b.dom.nodeValue=f.children);f.dom=b.dom;break;case "<":b.children!==f.children?(m(b),l(g,f,c)):(f.dom=b.dom,f.domSize=b.domSize);break;case "[":q(g,b.children,f.children,d,a,c,h);b=0;a=f.children;f.dom=null;if(null!=a){for(d=0;d Date: Tue, 5 Dec 2017 18:04:29 -0500 Subject: [PATCH 66/66] docs: Add flems version of sample application (#2049) --- docs/simple-application.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/simple-application.md b/docs/simple-application.md index cd943cc8..9ca3a698 100644 --- a/docs/simple-application.md +++ b/docs/simple-application.md @@ -2,6 +2,8 @@ Let's develop a simple application that covers some of the major aspects of Single Page Applications +An interactive running example can be seen here [flems: Simple Application](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcICGAHLA6AVmCADQgBmAljEagNqgB2GAthGiAKqQBOAsgPZJoBIqVj8GiSewBuGbgAIuERQF4FwADoMFuhVAph4qBbQC6xbXv38MSADKHjCsgFcGCChIAUASg1W1nrcEPCu3DrMuCEAjq4QRt5aOkGprPAAFoImmiAA4gCiACq5limp1uFQOSAZ8PBYYKgA9M0hzAC0IUYd2BS4GSr8ANau2HjizM19za48YKWBFXoA7hSZAMIhQpIUGFBNCvDc8WXLAL6+S0G4mRAM3m4e8F4P3a5Q8P7Jy9bK3LgDEYFOp3p9cEgMPAMNdrJdrucytdYOEQpITMBEdcoLYkCYnp4fBQkN9YcFQuFItEIHEEvAkmS0qEsniFLlCiUSIyglUanUGk1Wu0unTelh+oNuCMxjhcJNpuLZvNmrkFABqBTEs6-XRrTbbe4vfaHY6nbnw8o3O4PAkvHxgr4BS3Lf5y1GGkEKB3mq6WrEMa5gDAyCD49yEh6k53ksIRBRRWLxRI-HXx5nZNkgAAKHE52p1vMz-MaLTaEE63XgYolQ1G4zl-CmMzmKjAKpA6qUPDd3DR8FwWu51kh0JMrpRvcN+d+eoyW2Qhr2BxMpog07hvrh2nOIERjBYbHQ-0cRhEJBA4kkhtk8i7KhP8E9Kd0EgoDHWY+7OLsD+nMgoEArGGzyvH4TrLCEsaRN4uS4C23AdEC8ClHeAJIbgzDYI84Z2g88FRqmXoUnGzAwZgcE8IhTgdOs5YocAGQhGQNTNMg6ztp28EDkgxAKBIsAhFCobxtE-CuIggJvsMiIKFxlDcEYAByB6dqqqoalxUAYEpB6bhUlx6bo5zbruxD7qw7D-AAYvw3BRIQ56XlI8A3oo1m2cwT7XK+77OLaoEyAwggQN8rrfkg3iBcFuBQscYDcb4-rWP+gHARGYHPkEkGUvGZFkB59FDhUEhgK4ABGzAfi4OGgSF4GERUEC4FgIQhpIAAiEBkBgHz0oZDV-N2QYhn4RWpMZ0bjbxtBjbluRaWVwgLdAKG5FZFAKY+TCsLkvjrhUpG5G+WDiQODAnfAtDwAAnlgECqIgAAe8BmLQWBabAEBZFAQjcKo62bQo20QGYhWTb8PkXSYUSzgAgvU3BkXIUDxCh-k+Mj8Shd2E59rg8k6awnqYxAlz7TqJOfioPZ4wT8DKTt4MbuTQSHSAy1QICGCLVAq0gPY2lbQeu0s9YbPHadEuXe9GCfd9v2qALwLA6DJD1QNkPidDuBwwjSP7Kjavow8JPY9TuOGlzhMQMTBuk3ts3JXbVMAhbkhW-TwtM3oZOzWzZXifAEi4AH9QSFdt33aVFXrKrvG5AAysGEAi9yZj9RNO57iAwPsAL11if2DliBIzmuQo+eF15lopUB1UgRjQVCARFTZSRZGYW+XMF+JKEzd7uhs0wMgYfcrh947ehszCauZQNjFdSYADkzRIUvosQx4gmINrUriU1BgMMMk9GfHnDzLts3pxvg9kZAEYoVFQhyhkVBIGi-XWOnCImdnufoPWYuF5S7XnQAmQuTUWpdQoI9bwS8ADES9fTgP3t4JA-AUSsHdmVQQ10z6rycGDawuQCFGFyBibkaJfppVwhlWabdoKV3ErxUix4nC+E-j7BE04SFsXgM0VAxJyHqyyvcah9d0pPzqnPVuxFGEYB7vAFh3h3J2V4lImKCMwAcPNNw7cvhTLmUPCAOUYBRDAKvNIdAOCkB4LOhdYgIdA4SA0PldEQU7L7AUAARgAGxYEegoAAaioSETAADcmFuAAHM3wmAAEwAAYAkTW0N3KuwAomxIYKgbxyTAk9SDpEjAj0OhrCQJkXJiTqkBPCRNUeDBXAaCyXExJCg2kAGZ8l1O0Gk+CVFgTACQh0Iw10YCoCCgwCAxSYmtPaT47pWA7BIDfNE1AiSekMAoioAZVZaKeWAGVWWwxol7wYHieB3UrkYHCTg7g1DvEBIUGAfgBgkAKHgUgL54TxA4m4KgeBHSgXhJWWAGW11UBlRxLAYYMzsnrPmY8x64SllfNWagAAHE87xABWWpT0qxCHENwKErwJkSGmfU-pwz9moCyCGRQwACUdCJbZUlEhUDuF+ofSlvStkcw0KC8FkLoWwpaTktpbS8XIvqVLDQdyHlPJeW8j5XykC3Nsr9LodgKBzFQB02pODSlgAoAAL3RQqnZRqQWGGFVCjBYr5DwslQs2pqKVkMDWXk7F0rwnlMqXkxJABSTZTiw46EOcc05YlzkAogPGjV9yVC5KVa84kqrvmWoQiSlZeqDXIt+bZAFQKOk2rBVpCFb4eUdHtTCuFcy2neuRe69FTafG+uZaykluFyVTNDaHIOOT6UqHlVGs5FyIAYsnZOupu4LDsykjQegOcDzsEqpkbgVBzxVHYMWQUsxzonIbFMddjEqAAAFvG4Cvb45op7N2cyATdO67AHLnDMOcIAA) + First let's create an entry point for the application. Create a file `index.html`: ```markup