diff --git a/docs/keys.md b/docs/keys.md index 2c1e46d7..d6b16f8c 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -177,3 +177,30 @@ m("div", [ ] ]) ``` + +#### Avoid passing model data directly to components if the model uses `key` as a data property + +The `key` property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose `key` property is expected to change over time. This can lead to components receiving the wrong data, re-initialise, or change positions unexpectedly. If your data model uses the `key` property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction: + +```javascript +// Data model +var users = [ + {id: 1, name: "John", key: 'a'}, + {id: 2, name: "Mary", key: 'b'}, +] + +// Later on... +users[0].key = 'c' + +// AVOID +users.map(function(user){ + // The component for John will be destroyed and recreated + return m(UserComponent, user) +}) + +// PREFER +users.map(function(user){ + // Key is specifically extracted: data model is given its own property + return m(UserComponent, {key: user.id, model: user}) +}) +``` diff --git a/mithril.d.ts b/mithril.d.ts index 37179e16..8864d60a 100644 --- a/mithril.d.ts +++ b/mithril.d.ts @@ -238,6 +238,18 @@ declare namespace Mithril { /** A special value that can be returned to stream callbacks to halt execution of downstreams. */ HALT: any; } + + interface StreamScan { + /** Creates a new stream with the results of calling the function on every incoming stream with and accumulator and the incoming value. */ + (fn: (acc: U, value: T) => U, acc: U, stream: Stream): Stream; + } + + interface StreamScanMerge { + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + (pairs: [Stream, (acc: U, value: T) => U][], acc: U): Stream; + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + (pairs: [Stream, (acc: U, value: any) => U][], acc: U): Stream; + } } declare module 'mithril' { @@ -284,3 +296,13 @@ declare module 'mithril/stream' { const s: Mithril.StreamFactory; export = s; } + +declare module 'mithril/stream/scan' { + const s: Mithril.StreamScan; + export = s; +} + +declare module 'mithril/stream/scanMerge' { + const sm: Mithril.StreamScanMerge; + export = sm; +} diff --git a/mithril.js b/mithril.js index 5d3c46e5..b7f11d41 100644 --- a/mithril.js +++ b/mithril.js @@ -368,8 +368,8 @@ var coreRenderer = function($window) { } function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { case "#": return createText(parent, vnode, nextSibling) case "<": return createHTML(parent, vnode, nextSibling) @@ -442,7 +442,7 @@ var coreRenderer = function($window) { } return element } - function createComponent(parent, vnode, hooks, ns, nextSibling) { + function initComponent(vnode, hooks) { var sentinel if (typeof vnode.tag === "function") { vnode.state = null @@ -457,9 +457,13 @@ var coreRenderer = function($window) { if (sentinel.$$reentrantLock$$ != null) return $emptyFragment sentinel.$$reentrantLock$$ = true } + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) initLifecycle(vnode.state, vnode, hooks) vnode.instance = Vnode.normalize(vnode.state.view(vnode)) sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) @@ -570,11 +574,12 @@ var coreRenderer = function($window) { if (oldTag === tag) { vnode.state = old.state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) initLifecycle(vnode.attrs, vnode, hooks) + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break @@ -644,8 +649,13 @@ var coreRenderer = function($window) { } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.state.view(vnode)) - updateLifecycle(vnode.state, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode.state.view(vnode)) + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) + } if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) @@ -897,11 +907,10 @@ var coreRenderer = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + 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(vnode, old) diff --git a/mithril.min.js b/mithril.min.js index 732ad9c3..ec786ea9 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,43 +1,43 @@ -new function(){function w(a,d,h,f,g,l){return{tag:a,key:d,attrs:h,children:f,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){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&&void 0===H[a]){for(var d,h,f=[],g={};d=O.exec(a);){var l=d[1],k=d[2];""===l&&""!==k?h=k:"#"===l?g.id=k:"."===l?f.push(k):"["===d[3][0]&&((l=d[6])&&(l=l.replace(/\\(["'])/g, -"$1").replace(/\\\\/g,"\\")),"class"===d[4]?f.push(l):g[d[4]]=l||!0)}0c.indexOf("?")?"?":"&";c+=f+d}return c}function k(c){try{return""!==c?JSON.parse(c):null}catch(t){throw Error(c); -}}function q(c){return c.responseText}function p(c,a){if("function"===typeof c)if(Array.isArray(a))for(var d=0;dn.status||304===n.status||R.test(c.url))d(p(c.type, -b));else{var m=Error(n.responseText),a;for(a in b)m[a]=b[a];f(m)}}catch(C){f(C)}};h&&null!=c.data?n.send(c.data):n.send()});return!0===c.background?t:r(t)},jsonp:function(c,k){var q=h();c=f(c,k);var u=new d(function(d,f){var h=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,k=a.document.createElement("script");a[h]=function(f){k.parentNode.removeChild(k);d(p(c.type,f));delete a[h]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete a[h]};null== -c.data&&(c.data={});c.url=g(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?u:q(u)},setCompletionCallback:function(a){u=a}}}(window,v),N=function(a){function d(a,b,c,d,f,g,k){for(;c=m&&t>=e;){var y=b[m],z=c[e];if(y!==z||f)if(null==y)m++;else if(null==z)e++;else if(y.key===z.key){var C=null!=x&&m>=b.length-x.length||null==x&&f;m++;e++;l(a,y,z,g,q(b,m,B),C,n);f&&y.tag===z.tag&&p(a,k(y),B)}else if(y=b[r],y!==z||f)if(null==y)r--;else if(null==z)e++;else if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f, -l(a,y,z,g,q(b,r+1,B),C,n),(f||e=m&&t>=e;){y=b[r];z=c[t];if(y!==z||f)if(null==y)r--;else{if(null!=z)if(y.key===z.key)C=null!=x&&r>=b.length-x.length||null==x&&f,l(a,y,z,g,q(b,r+1,B),C,n),f&&y.tag===z.tag&&p(a,k(y),B),null!=y.dom&&(B=y.dom),r--;else{if(!E){E=b;var y=r,C={},v;for(v=0;va.indexOf("?")?"?":"&";a+=e+d}return a}function h(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a); +}}function r(a){return a.responseText}function n(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dm.status||304===m.status||R.test(a.url))d(n(a.type, +f));else{var c=Error(m.responseText),q;for(q in f)c[q]=f[q];e(c)}}catch(p){e(p)}};l&&null!=a.data?m.send(a.data):m.send()});return!0===a.background?y:v(y)},jsonp:function(a,h){var v=l();a=e(a,h);var r=new d(function(d,e){var l=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,h=b.document.createElement("script");b[l]=function(e){h.parentNode.removeChild(h);d(n(a.type,e));delete b[l]};h.onerror=function(){h.parentNode.removeChild(h);e(Error("JSONP request failed"));delete b[l]};null== +a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||"callback"]=l;h.src=k(a.url,a.data);b.document.documentElement.appendChild(h)});return!0===a.background?r:v(r)},setCompletionCallback:function(b){v=b}}}(window,A),N=function(b){function d(f,c,q,b,a,d,e){for(;q=q&&y>=p;){var z=c[q],t=b[p];if(z!==t||e)if(null==z)q++;else if(null==t)p++;else if(z.key===t.key){var u=null!=D&&q>=c.length-D.length||null==D&&e;q++;p++;h(f,z,t,g,n(c,q,k),u,v);e&&z.tag===t.tag&&m(f,r(z),k)}else if(z=c[w],z!==t||e)if(null==z)w--;else if(null==t)p++;else if(z.key===t.key)u= +null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),(e||p=q&&y>=p;){z=c[w];t=b[y];if(z!==t||e)if(null==z)w--;else{if(null!=t)if(z.key===t.key)u=null!=D&&w>=c.length-D.length||null==D&&e,h(f,z,t,g,n(c,w+1,k),u,v),e&&z.tag===t.tag&&m(f,r(z),k),null!=z.dom&&(k=z.dom),w--;else{if(!G){G=c;var z=w,u={},x;for(x=0;x