Merge remote-tracking branch 'origin/rewrite' into rewrite

# Conflicts:
#	mithril.min.js
This commit is contained in:
Leo Horie 2016-12-01 01:50:12 -05:00
commit 9f6a1086e4
10 changed files with 166 additions and 21 deletions

View file

@ -13,6 +13,12 @@ install:
- npm install
- npm install @alrra/travis-scripts@^3.0.1
# Lint (but don't fail build) before running tests
before_script: npm run lint || true
# Run more than just npm test
script: npm run build && npm test
# After a successful build create bundles & commit back to the repo
after_success:
- |
@ -29,8 +35,7 @@ after_success:
--path-encrypted-key "./.deploy.enc"
# Build & commit changes
$(npm bin)/commit-changes --commands "npm run build" \
--commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \
$(npm bin)/commit-changes --commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \
--branch "$BRANCH"
env:

View file

@ -173,7 +173,7 @@ Although Mithril is flexible, some code patterns are discouraged:
#### Avoid restrictive interfaces
A component has a restrictive interface when it exposes only specific properties, under the assumption that other properties will not be needed, or that they can be added at a later time.
Try to keep component interfaces generic - using `attrs` and `children` directly - unless the component requires special logic to operate on input.
In the example below, the `button` configuration is severely limited: it does not support any events other than `onclick`, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML).
@ -188,7 +188,7 @@ var RestrictiveComponent = {
}
```
It's preferable to allow passing through parameters to a component's root node, if it makes sense to do so:
If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node.
```javascript
// PREFER
@ -201,7 +201,9 @@ var FlexibleComponent = {
}
```
#### Avoid magic indexes
#### Don't manipulate `children`
However, if a component is opinionated in how it applies attributes or children, you should switch to using custom attributes.
Often it's desirable to define multiple sets of children, for example, if a component has a configurable title and body.
@ -233,7 +235,7 @@ m(Header, [
])
```
The component above makes different children look different based on where they appear in the array. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content:
The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content:
```javascript
// PREFER
@ -261,7 +263,9 @@ m(BetterHeader, {
})
```
#### Avoid component factories
#### Define components statically, call them dynamically
##### Avoid creating component definitions inside views
If you create a component from within a `view` method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch.
@ -291,3 +295,65 @@ m.render(document.body, m(Component, {greeting: "hello"}))
// calling a second time does not modify DOM
m.render(document.body, m(Component, {greeting: "hello"}))
```
##### Avoid creating component instances outside views
Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views:
```javascript
// AVOID
var Counter = {
count: 0,
view: function(vnode) {
return m("div",
m("p", "Count: " + vnode.state.count ),
m("button", {
onclick: function() {
vnode.state.count++
}
}, "Increase count")
)
}
}
var counter = m(Counter)
m.mount(document.body, {
view: function(vnode) {
return [
m("h1", "My app"),
counter
]
}
})
```
In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created:
```javascript
// PREFER
var Counter = {
count: 0,
view: function(vnode) {
return m("div",
m("p", "Count: " + vnode.state.count ),
m("button", {
onclick: function() {
vnode.state.count++
}
}, "Increase count")
)
}
}
m.mount(document.body, {
view: function(vnode) {
return [
m("h1", "My app"),
m(Counter)
]
}
})
```

View file

@ -408,3 +408,9 @@ var BetterListComponent = {
}
}
```
#### Avoid creating vnodes outside views
When a redraw encounters a vnode which is strictly equal to the one in the previous render, it will be skipped and its contents will not be updated. While this may seem like an opportunity for performance optimisation, it should be avoided because it prevents dynamic changes in that node's tree - this leads to side-effects such as downstream lifecycle methods failing to trigger on redraw. In this sense, Mithril vnodes are immutable: new vnodes are compared to old ones; mutations to vnodes are not persisted.
The component documentation contains [more detail and an example of this anti-pattern](components.md#avoid-creating-component-instances-outside-views).

View file

@ -10,6 +10,7 @@
- [Monitoring progress](#monitoring-progress)
- [Casting response to a type](#casting-response-to-a-type)
- [Non-JSON responses](#non-json-responses)
- [Retrieving response details](#retrieving-response-details)
- [Why JSON instead of HTML](#why-json-instead-of-html)
- [Why XMLHttpRequest instead of fetch](#why-xmlhttprequest-instead-of-fetch)

View file

@ -17,6 +17,8 @@
- [Accessing route params](#accessing-route-params)
- [`m.request`](#mrequest)
- [`xlink` namespace required](#xlink-namespace-required)
- [Nested arrays in views](#nested-arrays-in-views)
- [`vnode` equality checks](#vnode-equality-checks)
---
@ -243,7 +245,7 @@ m.mount(document.body, {
oninit : function(vnode) {
// ...
},
view : function(vnode) {
// Use vnode.state instead of ctrl
// Use vnode.attrs instead of options
@ -492,3 +494,15 @@ m("svg",
m("image[xlink:href='image.gif']")
)
```
---
## Nested arrays in views
Arrays now represent [fragments](fragment.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays.
---
## `vnode` equality checks
If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views).

View file

@ -16,7 +16,7 @@ The first time a virtual DOM tree is rendered, it is used as a blueprint to crea
Typically, Virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes.
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundres of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.

View file

@ -452,7 +452,6 @@ var coreRenderer = function($window) {
else {
var recycling = isRecyclable(old, vnodes)
if (recycling) old = old.concat(old.pool)
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
while (oldEnd >= oldStart && end >= start) {
var o = old[oldStart], v = vnodes[start]
@ -662,7 +661,7 @@ var coreRenderer = function($window) {
var content = children[0].children
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
}
else if (children != null || vnode.text != null) throw new Error("Child node of a contenteditable must be trusted")
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) {

12
mithril.min.js vendored
View file

@ -23,12 +23,12 @@ c.tag;if(g===e.tag){e.state=c.state;e.events=c.events;var y;var z;null!=e.attrs&
e.tag&&(null==e.attrs&&(e.attrs={}),null!=e.text&&(e.attrs.value=e.text,e.text=void 0));r=c.attrs;w=e.attrs;g=a;if(null!=w)for(p in w)u(e,p,r&&r[p],w[p],g);if(null!=r)for(p in r)null!=w&&p in w||("className"===p&&(p="class"),"o"!==p[0]||"n"!==p[1]||M(p)?"key"!==p&&e.dom.removeAttribute(p):F(e,p,void 0));null!=e.attrs&&null!=e.attrs.contenteditable?C(e):null!=c.text&&null!=e.text&&""!==e.text?c.text.toString()!==e.text.toString()&&(c.dom.firstChild.nodeValue=e.text):(null!=c.text&&(c.children=[n("#",
void 0,void 0,c.text,void 0,c.dom.firstChild)]),null!=e.text&&(e.children=[n("#",void 0,void 0,e.text,void 0,void 0)]),f(m,c.children,e.children,h,null,a))}else e.instance=n.normalize(e.tag.view.call(e.state,e)),A(e.tag,e,h,r),null!=e.instance?(null==c.instance?q(a,l(e.instance,h,w),m):k(a,c.instance,e.instance,h,m,r,w),e.dom=e.instance.dom,e.domSize=e.instance.domSize):null!=c.instance?(d(c.instance,null),e.dom=void 0,e.domSize=0):(e.dom=c.dom,e.domSize=c.domSize)}else d(c,null),q(a,l(e,h,w),m)}
function v(a){var g=a.domSize;if(null!=g||null==a.dom){var e=B.createDocumentFragment();if(0<g){for(a=a.dom;--g;)e.appendChild(a.nextSibling);e.insertBefore(a,e.firstChild)}return e}return a.dom}function p(a,c,e){for(;c<a.length;c++)if(null!=a[c]&&null!=a[c].dom)return a[c].dom;return e}function q(a,c,e){e&&e.parentNode?a.insertBefore(c,e):a.appendChild(c)}function C(a){var c=a.children;if(null!=c&&1===c.length&&"<"===c[0].tag)c=c[0].children,a.dom.innerHTML!==c&&(a.dom.innerHTML=c);else if(null!=
c||null!=a.text)throw Error("Child node of a contenteditable must be trusted");}function r(a,c,e,b){for(;c<e;c++){var g=a[c];null!=g&&(g.skip?g.skip=!1:d(g,b))}}function m(a){var c=!1;return function(){c||(c=!0,a())}}function d(a,c){function g(){if(++d===b&&(w(a),a.dom)){var g=a.domSize||1;if(1<g)for(var e=a.dom;--g;){var f=e.nextSibling,h=f.parentNode;null!=h&&h.removeChild(f)}g=a.dom;e=g.parentNode;null!=e&&e.removeChild(g);if(g=null!=c&&null==a.domSize)g=a.attrs,g=!(null!=g&&(g.oncreate||g.onupdate||
g.onbeforeremove||g.onremove));g&&"string"===typeof a.tag&&(c.pool?c.pool.push(a):c.pool=[a])}}var b=1,d=0;a.attrs&&a.attrs.onbeforeremove&&(b++,a.attrs.onbeforeremove.call(a.state,a,m(g)));"string"!==typeof a.tag&&a.tag.onbeforeremove&&(b++,a.tag.onbeforeremove.call(a.state,a,m(g)));g()}function w(a){a.attrs&&a.attrs.onremove&&a.attrs.onremove.call(a.state,a);"string"!==typeof a.tag&&a.tag.onremove&&a.tag.onremove.call(a.state,a);if(null!=a.instance)w(a.instance);else if(a=a.children,a instanceof
Array)for(var c=0;c<a.length;c++){var g=a[c];null!=g&&w(g)}}function u(a,c,e,b,d){var g=a.dom;if("key"!==c&&(e!==b||"value"===c||"checked"===c||"selectedIndex"===c||"selected"===c&&a.dom===B.activeElement||"object"===typeof b)&&"undefined"!==typeof b&&!M(c)){var f=c.indexOf(":");if(-1<f&&"xlink"===c.substr(0,f))g.setAttributeNS("http://www.w3.org/1999/xlink",c.slice(f+1),b);else if("o"===c[0]&&"n"===c[1]&&"function"===typeof b)F(a,c,b);else if("style"===c)if(a=e,a===b&&(g.style.cssText="",a=null),
null==b)g.style.cssText="";else if("string"===typeof b)g.style.cssText=b;else{"string"===typeof a&&(g.style.cssText="");for(var h in b)g.style[h]=b[h];if(null!=a&&"string"!==typeof a)for(h in a)h in b||(g.style[h]="")}else c in g&&"href"!==c&&"list"!==c&&"form"!==c&&"width"!==c&&"height"!==c&&void 0===d?"input"===a.tag&&"value"===c&&a.dom.value===b&&a.dom===B.activeElement||"select"===a.tag&&"value"===c&&a.dom.value===b&&a.dom===B.activeElement||"option"===a.tag&&"value"===c&&a.dom.value===b||(g[c]=
b):"boolean"===typeof b?b?g.setAttribute(c,""):g.removeAttribute(c):g.setAttribute("className"===c?"class":c,b)}}function M(a){return"oninit"===a||"oncreate"===a||"onupdate"===a||"onremove"===a||"onbeforeremove"===a||"onbeforeupdate"===a}function F(a,c,b){var e=a.dom,g=function(a){var c=b.call(e,a);"function"===typeof K&&K.call(e,a);return c};if(c in e)e[c]="function"===typeof b?g:null;else{var d=c.slice(2);void 0===a.events&&(a.events={});null!=a.events[c]&&e.removeEventListener(d,a.events[c],!1);
"function"===typeof b&&(a.events[c]=g,e.addEventListener(d,a.events[c],!1))}}function t(a,c,b){"function"===typeof a.oninit&&a.oninit.call(c.state,c);"function"===typeof a.oncreate&&b.push(a.oncreate.bind(c.state,c))}function A(a,c,b,d){d?t(a,c,b):"function"===typeof a.onupdate&&b.push(a.onupdate.bind(c.state,c))}function E(a,c){Object.keys(c).forEach(function(b){a[b]=c[b]})}var B=a.document,H=B.createDocumentFragment(),K;return{render:function(a,c){if(!a)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
a.text||null!=c&&0!==c.length)throw Error("Child node of a contenteditable must be trusted");}function r(a,c,e,b){for(;c<e;c++){var g=a[c];null!=g&&(g.skip?g.skip=!1:d(g,b))}}function m(a){var c=!1;return function(){c||(c=!0,a())}}function d(a,c){function g(){if(++d===b&&(w(a),a.dom)){var g=a.domSize||1;if(1<g)for(var e=a.dom;--g;){var f=e.nextSibling,h=f.parentNode;null!=h&&h.removeChild(f)}g=a.dom;e=g.parentNode;null!=e&&e.removeChild(g);if(g=null!=c&&null==a.domSize)g=a.attrs,g=!(null!=g&&(g.oncreate||
g.onupdate||g.onbeforeremove||g.onremove));g&&"string"===typeof a.tag&&(c.pool?c.pool.push(a):c.pool=[a])}}var b=1,d=0;a.attrs&&a.attrs.onbeforeremove&&(b++,a.attrs.onbeforeremove.call(a.state,a,m(g)));"string"!==typeof a.tag&&a.tag.onbeforeremove&&(b++,a.tag.onbeforeremove.call(a.state,a,m(g)));g()}function w(a){a.attrs&&a.attrs.onremove&&a.attrs.onremove.call(a.state,a);"string"!==typeof a.tag&&a.tag.onremove&&a.tag.onremove.call(a.state,a);if(null!=a.instance)w(a.instance);else if(a=a.children,
a instanceof Array)for(var c=0;c<a.length;c++){var g=a[c];null!=g&&w(g)}}function u(a,c,e,b,d){var g=a.dom;if("key"!==c&&(e!==b||"value"===c||"checked"===c||"selectedIndex"===c||"selected"===c&&a.dom===B.activeElement||"object"===typeof b)&&"undefined"!==typeof b&&!M(c)){var f=c.indexOf(":");if(-1<f&&"xlink"===c.substr(0,f))g.setAttributeNS("http://www.w3.org/1999/xlink",c.slice(f+1),b);else if("o"===c[0]&&"n"===c[1]&&"function"===typeof b)F(a,c,b);else if("style"===c)if(a=e,a===b&&(g.style.cssText=
"",a=null),null==b)g.style.cssText="";else if("string"===typeof b)g.style.cssText=b;else{"string"===typeof a&&(g.style.cssText="");for(var h in b)g.style[h]=b[h];if(null!=a&&"string"!==typeof a)for(h in a)h in b||(g.style[h]="")}else c in g&&"href"!==c&&"list"!==c&&"form"!==c&&"width"!==c&&"height"!==c&&void 0===d?"input"===a.tag&&"value"===c&&a.dom.value===b&&a.dom===B.activeElement||"select"===a.tag&&"value"===c&&a.dom.value===b&&a.dom===B.activeElement||"option"===a.tag&&"value"===c&&a.dom.value===
b||(g[c]=b):"boolean"===typeof b?b?g.setAttribute(c,""):g.removeAttribute(c):g.setAttribute("className"===c?"class":c,b)}}function M(a){return"oninit"===a||"oncreate"===a||"onupdate"===a||"onremove"===a||"onbeforeremove"===a||"onbeforeupdate"===a}function F(a,c,b){var e=a.dom,g=function(a){var c=b.call(e,a);"function"===typeof K&&K.call(e,a);return c};if(c in e)e[c]="function"===typeof b?g:null;else{var d=c.slice(2);void 0===a.events&&(a.events={});null!=a.events[c]&&e.removeEventListener(d,a.events[c],
!1);"function"===typeof b&&(a.events[c]=g,e.addEventListener(d,a.events[c],!1))}}function t(a,c,b){"function"===typeof a.oninit&&a.oninit.call(c.state,c);"function"===typeof a.oncreate&&b.push(a.oncreate.bind(c.state,c))}function A(a,c,b,d){d?t(a,c,b):"function"===typeof a.onupdate&&b.push(a.onupdate.bind(c.state,c))}function E(a,c){Object.keys(c).forEach(function(b){a[b]=c[b]})}var B=a.document,H=B.createDocumentFragment(),K;return{render:function(a,c){if(!a)throw Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
var b=[],d=B.activeElement;null==a.vnodes&&(a.textContent="");c instanceof Array||(c=[c]);f(a,a.vnodes,n.normalizeChildren(c),b,null,void 0);a.vnodes=c;for(var g=0;g<b.length;g++)b[g]();B.activeElement!==d&&d.focus()},setEventCallback:function(a){return K=a}}},E=function(a){function h(a){a=b.indexOf(a);-1<a&&b.splice(a,2)}function l(){for(var a=1;a<b.length;a+=2)b[a]()}a=O(a);a.setEventCallback(function(a){!1!==a.redraw&&l()});var b=[];return{subscribe:function(a,k){h(a);b.push(a,k)},unsubscribe:h,
redraw:l,render:a.render}}(window);J.setCompletionCallback(E.redraw);A.mount=function(a){function h(a){var b=0,f=null,h="function"===typeof requestAnimationFrame?requestAnimationFrame:setTimeout;return function(){var k=Date.now();0===b||16<=k-b?(b=k,a()):null===f&&(f=h(function(){f=null;a();b=Date.now()},16-(k-b)))}}return function(l,b){if(null===b)a.render(l,[]),a.unsubscribe(l);else{if(null==b.view)throw Error("m.mount(element, component) expects a component, not a vnode");var f=h(function(){a.render(l,
n(b))});a.subscribe(l,f);f()}}}(E);var L=function(a){if(""===a||null==a)return{};"?"===a.charAt(0)&&(a=a.slice(1));a=a.split("&");for(var h={},l={},b=0;b<a.length;b++){var f=a[b].split("="),k=decodeURIComponent(f[0]),f=2===f.length?decodeURIComponent(f[1]):"";"true"===f?f=!0:"false"===f&&(f=!1);var n=k.split(/\]\[?|\[/),p=h;-1<k.indexOf("[")&&n.pop();for(var q=0;q<n.length;q++){var k=n[q],t=n[q+1],t=""==t||!isNaN(parseInt(t,10)),r=q===n.length-1;""===k&&(k=n.slice(0,q).join(),null==l[k]&&(l[k]=0),

View file

@ -135,7 +135,7 @@ module.exports = function($window) {
else {
var recycling = isRecyclable(old, vnodes)
if (recycling) old = old.concat(old.pool)
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
while (oldEnd >= oldStart && end >= start) {
var o = old[oldStart], v = vnodes[start]
@ -347,7 +347,7 @@ module.exports = function($window) {
var content = children[0].children
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
}
else if (children != null || vnode.text != null) throw new Error("Child node of a contenteditable must be trusted")
else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted")
}
//remove

View file

@ -79,9 +79,9 @@ o.spec("attributes", function() {
o.spec("canvas width and height", function() {
o("uses attribute API", function() {
var canvas = {tag: "canvas", attrs: {width: "100%"}}
render(root, canvas)
o(canvas.dom.attributes["width"].nodeValue).equals("100%")
o(canvas.dom.width).equals(100)
})
@ -95,4 +95,58 @@ o.spec("attributes", function() {
o(a.dom.attributes["class"].nodeValue).equals("test")
})
})
o.spec("contenteditable throws on untrusted children", function() {
o("including text nodes", function() {
var div = {tag: "div", attrs: {contenteditable: true}, text: ''}
var succeeded = false
try {
render(root, div)
succeeded = true
}
catch(e){}
o(succeeded).equals(false)
})
o("including elements", function() {
var div = {tag: "div", attrs: {contenteditable: true}, children: [{tag: "script", attrs: {src: "http://evil.com"}}]}
var succeeded = false
try {
render(root, div)
succeeded = true
}
catch(e){}
o(succeeded).equals(false)
})
o("tolerating empty children", function() {
var div = {tag: "div", attrs: {contenteditable: true}, children: []}
var succeeded = false
try {
render(root, div)
succeeded = true
}
catch(e){}
o(succeeded).equals(true)
})
o("tolerating trusted content", function() {
var div = {tag: "div", attrs: {contenteditable: true}, children: [{tag: "<", children: "<a></a>"}]}
var succeeded = false
try {
render(root, div)
succeeded = true
}
catch(e){}
o(succeeded).equals(true)
})
})
})