From d82d337569f1adf5a94238e0fe1bd27797395379 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Thu, 24 Nov 2016 09:21:27 +0000 Subject: [PATCH 01/38] Replace x instanceof Array with Array.isArray(x) --- README.md | 2 +- mithril.js | 18 ++++++++---------- mithril.min.js | 18 +++++++++--------- ospec/ospec.js | 4 ++-- querystring/build.js | 2 +- render/hyperscript.js | 6 +++--- render/render.js | 6 +++--- render/tests/test-fragment.js | 4 ++-- render/vnode.js | 2 +- request/request.js | 4 ++-- 10 files changed, 32 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index f748cef7..71c22280 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.40 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.41 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 7fdbec3a..520055e5 100644 --- a/mithril.js +++ b/mithril.js @@ -4,7 +4,7 @@ function Vnode(tag, key, attrs0, children, text, dom) { return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { - if (node instanceof Array) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined) return node } @@ -52,19 +52,19 @@ function hyperscript(selector) { break } } - if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children + if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children else childList = children return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined) } } var attrs, children, childrenIndex - if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !(arguments[1] instanceof Array)) { + if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) { attrs = arguments[1] childrenIndex = 2 } else childrenIndex = 1 if (arguments.length === childrenIndex + 1) { - children = arguments[childrenIndex] instanceof Array ? arguments[childrenIndex] : [arguments[childrenIndex]] + children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]] } else { children = [] @@ -187,7 +187,7 @@ var buildQueryString = function(object) { } return args.join("&") function destructure(key0, value) { - if (value instanceof Array) { + if (Array.isArray(value)) { for (var i = 0; i < value.length; i++) { destructure(key0 + "[" + i + "]", value[i]) } @@ -219,7 +219,6 @@ var _8 = function($window, Promise) { } return promise0 } - function request(args, extra) { return finalize(new Promise(function(resolve, reject) { if (typeof args === "string") { @@ -316,7 +315,7 @@ var _8 = function($window, Promise) { function extract(xhr) {return xhr.responseText} function cast(type0, data) { if (typeof type0 === "function") { - if (data instanceof Array) { + if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { data[i] = new type0(data[i]) } @@ -467,7 +466,6 @@ var _13 = 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] @@ -739,7 +737,7 @@ var _13 = function($window) { if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children - if (children instanceof Array) { + if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i] if (child != null) onremove(child) @@ -880,7 +878,7 @@ var _13 = function($window) { var active = $doc.activeElement // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - if (!(vnodes instanceof Array)) vnodes = [vnodes] + if (!Array.isArray(vnodes)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) dom.vnodes = vnodes for (var i = 0; i < hooks.length; i++) hooks[i]() diff --git a/mithril.min.js b/mithril.min.js index 41590c4a..5a8f0d69 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,13 +1,13 @@ new function(){function m(a,b,k,e,l,h){return{tag:a,key:b,attrs:k,children:e,text:l,dom:h,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function t(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===H[a]){for(var b,k,e=[],l={};b=O.exec(a);){var h=b[1],v=b[2];""===h&&""!==v?k=v:"#"===h?l.id=v:"."===h?e.push(v):"["===b[3][0]&&((h=b[6])&&(h=h.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===b[4]?e.push(h):l[b[4]]=h||!0)}0a.indexOf("?")?"?":"&";a+=e+f}return a}function v(a){try{return""!== -a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function r(a,b){if("function"===typeof a)if(b instanceof Array)for(var e=0;ea.indexOf("?")?"?":"&";a+=e+f}return a}function v(a){try{return""!== +a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function r(a,b){if("function"===typeof a)if(Array.isArray(b))for(var e=0;en.status||304===n.status)b(r(f.type,a));else{var h=Error(n.responseText),k;for(k in a)h[k]=a[k];e(h)}}catch(G){e(G)}}; u&&null!=f.data?n.send(f.data):n.send()}))},jsonp:function(f){return e(new b(function(b,e){var n=f.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[n]=function(e){k.parentNode.removeChild(k);b(r(f.type,e));delete a[n]};k.onerror=function(){k.parentNode.removeChild(k);e(Error("JSONP request failed"));delete a[n]};null==f.data&&(f.data={});f.url=l(f.url,f.data);f.data[f.callbackKey||"callback"]=n;k.src=h(f.url,f.data);a.document.documentElement.appendChild(k)}))}, @@ -25,11 +25,11 @@ d.attrs&&null!=d.attrs.contenteditable?y(d):null!=g.text&&null!=d.text&&""!==d.t b,u,q,w),d.dom=d.instance.dom,d.domSize=d.instance.domSize):null!=g.instance?(f(g.instance,null),d.dom=void 0,d.domSize=0):(d.dom=g.dom,d.domSize=g.domSize)}else f(g,null),r(a,k(d,b,w),u)}function v(a){var c=a.domSize;if(null!=c||null==a.dom){var d=z.createDocumentFragment();if(0" try {return JSON.stringify(value)} catch (e) {return String(value)} } diff --git a/querystring/build.js b/querystring/build.js index 3bea081f..55474392 100644 --- a/querystring/build.js +++ b/querystring/build.js @@ -10,7 +10,7 @@ module.exports = function(object) { return args.join("&") function destructure(key, value) { - if (value instanceof Array) { + if (Array.isArray(value)) { for (var i = 0; i < value.length; i++) { destructure(key + "[" + i + "]", value[i]) } diff --git a/render/hyperscript.js b/render/hyperscript.js index 28ce0d35..a2cf7332 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -41,20 +41,20 @@ function hyperscript(selector) { break } } - if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children + if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children else childList = children return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined) } } var attrs, children, childrenIndex - if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !(arguments[1] instanceof Array)) { + if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) { attrs = arguments[1] childrenIndex = 2 } else childrenIndex = 1 if (arguments.length === childrenIndex + 1) { - children = arguments[childrenIndex] instanceof Array ? arguments[childrenIndex] : [arguments[childrenIndex]] + children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]] } else { children = [] diff --git a/render/render.js b/render/render.js index 909477a2..a851c91f 100644 --- a/render/render.js +++ b/render/render.js @@ -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] @@ -410,7 +410,7 @@ module.exports = function($window) { if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children - if (children instanceof Array) { + if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i] if (child != null) onremove(child) @@ -559,7 +559,7 @@ module.exports = function($window) { // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - if (!(vnodes instanceof Array)) vnodes = [vnodes] + if (!Array.isArray(vnodes)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) dom.vnodes = vnodes for (var i = 0; i < hooks.length; i++) hooks[i]() diff --git a/render/tests/test-fragment.js b/render/tests/test-fragment.js index f0403bbc..b10fc850 100644 --- a/render/tests/test-fragment.js +++ b/render/tests/test-fragment.js @@ -11,7 +11,7 @@ o.spec("fragment", function() { o(frag.tag).equals("[") - o(frag.children instanceof Array).equals(true) + o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(1) o(frag.children[0]).equals(child) @@ -24,7 +24,7 @@ o.spec("fragment", function() { var frag = fragment(attrs, []) o(frag.tag).equals("[") - o(frag.children instanceof Array).equals(true) + o(Array.isArray(frag.children)).equals(true) o(frag.children.length).equals(0) o(frag.attrs).equals(attrs) diff --git a/render/vnode.js b/render/vnode.js index 0c52d53d..99877e4e 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -2,7 +2,7 @@ function Vnode(tag, key, attrs, children, text, dom) { return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { - if (node instanceof Array) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined) return node } diff --git a/request/request.js b/request/request.js index a005f240..91ea17a3 100644 --- a/request/request.js +++ b/request/request.js @@ -23,7 +23,7 @@ module.exports = function($window, Promise) { } return promise } - + function request(args, extra) { return finalize(new Promise(function(resolve, reject) { if (typeof args === "string") { @@ -136,7 +136,7 @@ module.exports = function($window, Promise) { function cast(type, data) { if (typeof type === "function") { - if (data instanceof Array) { + if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { data[i] = new type(data[i]) } From a53515404317fb36239b1b1c195bae1ab55ff0be Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 16 Dec 2016 10:58:25 +0000 Subject: [PATCH 02/38] Bundle merge aa72f87 --- README.md | 2 +- mithril.min.js | 80 +++++++++++++++++++++++++------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 11a87ed7..4f1d161b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.47 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.48 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.min.js b/mithril.min.js index 69e76332..02a8ce63 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ new function(){function w(a,c,h,d,g,m){return{tag:a,key:c,attrs:h,children:d,text:g,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function z(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===K[a]){for(var c,h,d=[],g={};c=P.exec(a);){var m=c[1],l=c[2];""===m&&""!==l?h=l:"#"===m?g.id=l:"."===m?d.push(l):"["===c[3][0]&&((m=c[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(m):g[c[4]]=m||!0)}0a.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a, -c){if("function"===typeof a)if(c instanceof Array)for(var b=0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c, -d){var h=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window, -H),O=function(a){function c(e,f,a,b,c,d,g){for(;a=k&&t>=A;){var x=f[k],u=a[A];if(x!==u||q)if(null==x)k++;else if(null==u)A++;else if(x.key===u.key)k++,A++,m(e,x,u,b,n(f,k,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d);else if(x=f[y],x!==u||q)if(null==x)y--;else if(null==u)A++;else if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),(q||A=k&&t>=A;){x=f[y];u=a[t];if(x!==u||q)if(null==x)y--;else{if(null!= -u)if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d),null!=x.dom&&(d=x.dom),y--;else{if(!D){D=f;var x=y,C={},w;for(w=0;wa.indexOf("?")?"?":"&";a+=d+b}return a}function l(a){try{return""!==a?JSON.parse(a):null}catch(D){throw Error(a);}}function n(a){return a.responseText}function p(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b= +0;bk.status||304===k.status)c(p(b.type,a));else{var g=Error(k.responseText),h;for(h in a)g[h]=a[h];d(g)}}catch(F){d(F)}};h&&null!=b.data?k.send(b.data):k.send()});return!0===b.background?t:r(t)},jsonp:function(b,l){var n=h();b=d(b,l);var t=new c(function(c,d){var h=b.callbackName||"_mithril_"+Math.round(1E16* +Math.random())+"_"+k++,l=a.document.createElement("script");a[h]=function(d){l.parentNode.removeChild(l);c(p(b.type,d));delete a[h]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[h]};null==b.data&&(b.data={});b.url=g(b.url,b.data);b.data[b.callbackKey||"callback"]=h;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?t:n(t)},setCompletionCallback:function(a){r=a}}}(window,H),O=function(a){function c(e,f,a,b,c,d,g){for(;a< +b;a++){var q=f[a];null!=q&&p(e,h(q,c,g),d)}}function h(e,f,a){var q=e.tag;null!=e.attrs&&L(e.attrs,e,f);if("string"===typeof q)switch(q){case "#":return e.dom=B.createTextNode(e.children);case "<":return d(e);case "[":var b=B.createDocumentFragment();null!=e.children&&(q=e.children,c(b,q,0,q.length,f,null,a));e.dom=b.firstChild;e.domSize=b.childNodes.length;return b;default:var g=e.tag;switch(e.tag){case "svg":a="http://www.w3.org/2000/svg";break;case "math":a="http://www.w3.org/1998/Math/MathML"}var p= +(q=e.attrs)&&q.is,g=a?p?B.createElementNS(a,g,{is:p}):B.createElementNS(a,g):p?B.createElement(g,{is:p}):B.createElement(g);e.dom=g;if(null!=q)for(b in p=a,q)t(e,b,null,q[b],p);null!=e.attrs&&null!=e.attrs.contenteditable?k(e):(null!=e.text&&(""!==e.text?g.textContent=e.text:e.children=[w("#",void 0,void 0,e.text,void 0,void 0)]),null!=e.children&&(b=e.children,c(g,b,0,b.length,f,null,a),f=e.attrs,"select"===e.tag&&null!=f&&("value"in f&&t(e,"value",null,f.value,void 0),"selectedIndex"in f&&t(e,"selectedIndex", +null,f.selectedIndex,void 0))));return g}else{e.state||(e.state={});R(e.state,e.tag);b=e.tag.view;if(null!=b.reentrantLock)e=E;else if(b.reentrantLock=!0,L(e.tag,e,f),e.instance=w.normalize(b.call(e.state,e)),b.reentrantLock=null,null!=e.instance){if(e.instance===e)throw Error("A view cannot return the vnode it received as arguments");f=h(e.instance,f,a);e.dom=e.instance.dom;e.domSize=null!=e.dom?e.instance.domSize:0;e=f}else e.domSize=0,e=E;return e}}function d(e){var f={caption:"table",thead:"table", +tbody:"table",tfoot:"table",tr:"tbody",th:"tr",td:"tr",colgroup:"table",col:"colgroup"}[(e.children.match(/^\s*?<(\w+)/im)||[])[1]]||"div",f=B.createElement(f);f.innerHTML=e.children;e.dom=f.firstChild;e.domSize=f.childNodes.length;e=B.createDocumentFragment();for(var a;a=f.firstChild;)e.appendChild(a);return e}function g(e,f,a,b,d,g){if(f!==a&&(null!=f||null!=a))if(null==f)c(e,a,0,a.length,b,d,void 0);else if(null==a)r(f,0,f.length,a);else{for(var q=!1,k=0;k=k&&t>=A;){var x=f[k],u=a[A];if(x!==u||q)if(null==x)k++;else if(null==u)A++;else if(x.key===u.key)k++,A++,m(e,x,u,b,n(f,k,d),q,g),q&&x.tag===u.tag&&p(e,l(x),d);else if(x=f[y],x!==u||q)if(null==x)y--;else if(null==u)A++;else if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),(q||A=k&&t>=A;){x=f[y];u=a[t];if(x!==u||q)if(null==x)y--;else{if(null!=u)if(x.key===u.key)m(e,x,u,b,n(f,y+1,d),q,g),q&&x.tag===u.tag&&p(e,l(x), +d),null!=x.dom&&(d=x.dom),y--;else{if(!D){D=f;var x=y,C={},w;for(w=0;w Date: Fri, 30 Dec 2016 19:31:51 +0000 Subject: [PATCH 03/38] Test for onbeforeremove delayed resolution --- render/tests/test-onbeforeremove.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 82a859d0..c9af4894 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -158,14 +158,14 @@ o.spec("onbeforeremove", function() { render(root, vnodes) render(root, updated) - + o(root.childNodes.length).equals(2) o(root.firstChild.firstChild.nodeValue).equals("1") - + callAsync(function() { o(root.childNodes.length).equals(1) o(root.firstChild.firstChild.nodeValue).equals("2") - + done() }) }) @@ -184,4 +184,25 @@ o.spec("onbeforeremove", function() { done() }) }) + o("awaits promise resolution before removing the node", function(done) { + var view = o.spy() + var onremove = o.spy() + var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} + var component = { + onbeforeremove: onbeforeremove, + onremove: onremove, + view: view, + } + render(root, [{tag: component}]) + render(root, []) + + callAsync(function(){ + o(onremove.callCount).equals(0) + + callAsync(function() { + o(onremove.callCount).equals(1) + done() + }) + }) + }) }) From 33fb63a72ba585fdf51bdd319e4a9a68829b1bc1 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Tue, 3 Jan 2017 10:47:35 -0500 Subject: [PATCH 04/38] fix merge --- docs/installation.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 2d20dd11..6993c5d5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,10 +38,6 @@ npm run build open index.html ``` -The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. -Any changes in the source files will instantly get recompiled and the -browser will refresh reflecting the changes. - #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. @@ -166,6 +162,8 @@ npm install budo -g npm start ``` +The source file `index.js` will be compiled (bundled) and a browser window opens showing the result. Any changes in the source files will instantly get recompiled and the browser will refresh reflecting the changes. + #### Mithril bundler Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. From 3e3834a762a8b2e37932aeef58e18a8d5a2bf4ca Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Wed, 4 Jan 2017 14:56:55 +0000 Subject: [PATCH 05/38] Bundled output for commit f149003d034dd9880fc3ae54094f0d02c399ab3b [skip ci] --- README.md | 2 +- mithril.js | 16 +++++----- mithril.min.js | 82 +++++++++++++++++++++++++------------------------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index b6e99098..466958b4 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.59 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.58 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 55810181..29e9e17c 100644 --- a/mithril.js +++ b/mithril.js @@ -4,7 +4,7 @@ function Vnode(tag, key, attrs0, children, text, dom) { return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { - if (node instanceof Array) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) + if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined) return node } @@ -52,19 +52,19 @@ function hyperscript(selector) { break } } - if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children + if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children else childList = children return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined) } } var attrs, children, childrenIndex - if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !(arguments[1] instanceof Array)) { + if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) { attrs = arguments[1] childrenIndex = 2 } else childrenIndex = 1 if (arguments.length === childrenIndex + 1) { - children = arguments[childrenIndex] instanceof Array ? arguments[childrenIndex] : [arguments[childrenIndex]] + children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]] } else { children = [] @@ -190,7 +190,7 @@ var buildQueryString = function(object) { } return args.join("&") function destructure(key0, value) { - if (value instanceof Array) { + if (Array.isArray(value)) { for (var i = 0; i < value.length; i++) { destructure(key0 + "[" + i + "]", value[i]) } @@ -332,7 +332,7 @@ var _8 = function($window, Promise) { function extract(xhr) {return xhr.responseText} function cast(type0, data) { if (typeof type0 === "function") { - if (data instanceof Array) { + if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { data[i] = new type0(data[i]) } @@ -746,7 +746,7 @@ var coreRenderer = function($window) { if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children - if (children instanceof Array) { + if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i] if (child != null) onremove(child) @@ -888,7 +888,7 @@ var coreRenderer = function($window) { var active = $doc.activeElement // First time0 rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" - if (!(vnodes instanceof Array)) vnodes = [vnodes] + if (!Array.isArray(vnodes)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) dom.vnodes = vnodes for (var i = 0; i < hooks.length; i++) hooks[i]() diff --git a/mithril.min.js b/mithril.min.js index ddf9415f..71b834ca 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,42 +1,42 @@ new function(){function w(b,c,h,d,g,l){return{tag:b,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(b){if(null==b||"string"!==typeof b&&null==b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b&&void 0===G[b]){for(var c,h,d=[],g={};c=N.exec(b);){var l=c[1],m=c[2];""===l&&""!==m?h=m:"#"===l?g.id=m:"."===l?d.push(m):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+c}return a}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b); -}}function q(b){return b.responseText}function n(b,c){if("function"===typeof b)if(c instanceof Array)for(var a=0;ak.status||304===k.status)c(n(a.type,b));else{var g=Error(k.responseText),e;for(e in b)g[e]=b[e]; -d(g)}}catch(f){d(f)}};h&&null!=a.data?k.send(a.data):k.send()});return!0===a.background?z:t(z)},jsonp:function(a,m){var t=h();a=d(a,m);var q=new c(function(c,d){var h=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,m=b.document.createElement("script");b[h]=function(d){m.parentNode.removeChild(m);c(n(a.type,d));delete b[h]};m.onerror=function(){m.parentNode.removeChild(m);d(Error("JSONP request failed"));delete b[h]};null==a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey|| -"callback"]=h;m.src=l(a.url,a.data);b.document.documentElement.appendChild(m)});return!0===a.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,x),M=function(b){function c(e,f,b,a,c,d,g){for(;b=u&&z>=t;){var y=f[u],p=b[t];if(y!==p||r)if(null==y)u++;else if(null==p)t++;else if(y.key===p.key)u++,t++,m(e,y,p,d,n(f,u,g),r,l),r&&y.tag=== -p.tag&&k(e,q(y),g);else if(y=f[v],y!==p||r)if(null==y)v--;else if(null==p)t++;else if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),(r||t=u&&z>=t;){y=f[v];p=b[z];if(y!==p||r)if(null==y)v--;else{if(null!=p)if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!C){C=f;var y=v,E={},w;for(w=0;wa.indexOf("?")?"?":"&";a+=d+c}return a}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b);}}function q(b){return b.responseText} +function n(b,c){if("function"===typeof b)if(Array.isArray(c))for(var a=0;ak.status||304===k.status)c(n(a.type,b));else{var g=Error(k.responseText),e;for(e in b)g[e]=b[e];d(g)}}catch(f){d(f)}};h&&null!=a.data?k.send(a.data):k.send()}); +return!0===a.background?z:t(z)},jsonp:function(a,m){var t=h();a=d(a,m);var q=new c(function(c,d){var h=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,m=b.document.createElement("script");b[h]=function(d){m.parentNode.removeChild(m);c(n(a.type,d));delete b[h]};m.onerror=function(){m.parentNode.removeChild(m);d(Error("JSONP request failed"));delete b[h]};null==a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||"callback"]=h;m.src=l(a.url,a.data);b.document.documentElement.appendChild(m)}); +return!0===a.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,x),M=function(b){function c(e,f,b,a,c,d,g){for(;b=u&&z>=t;){var y=f[u],p=b[t];if(y!==p||r)if(null==y)u++;else if(null==p)t++;else if(y.key===p.key)u++,t++,m(e,y,p,d,n(f,u,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g);else if(y=f[v],y!==p||r)if(null==y)v--;else if(null==p)t++;else if(y.key===p.key)m(e, +y,p,d,n(f,v+1,g),r,l),(r||t=u&&z>=t;){y=f[v];p=b[z];if(y!==p||r)if(null==y)v--;else{if(null!=p)if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!C){C=f;var y=v,E={},w;for(w=0;w Date: Thu, 5 Jan 2017 15:08:43 +0200 Subject: [PATCH 06/38] Remove extra code from example that is not required, since it's confusingly similar to previous block --- docs/route.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/route.md b/docs/route.md index c443d418..35549125 100644 --- a/docs/route.md +++ b/docs/route.md @@ -545,9 +545,9 @@ m.route(document.body, "/user/list", { "/user/list": { onmatch: state.loadUsers, render: function() { - return state.users.length > 0 ? state.users.map(function(user) { + return state.users.map(function(user) { return m("div", user.id) - }) : "loading" + }) } }, }) From 6170573c290e980987cb1b1d9b78631e1b633f83 Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Thu, 5 Jan 2017 23:07:34 -0800 Subject: [PATCH 07/38] feat: Return empty string node for `false` values Very specifically doing a strict `false` check here to try & avoid coercion perf costs. --- render/vnode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render/vnode.js b/render/vnode.js index 99877e4e..56df8c81 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -3,7 +3,7 @@ function Vnode(tag, key, attrs, children, text, dom) { } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) - if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined) + if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined) return node } Vnode.normalizeChildren = function normalizeChildren(children) { From 9f32267259a51db1f3a0dcc8161cf8f219078eec Mon Sep 17 00:00:00 2001 From: Pat Cavit Date: Thu, 5 Jan 2017 23:07:47 -0800 Subject: [PATCH 08/38] tests: Update tests for false -> "" behavior --- render/tests/test-component.js | 6 +++--- render/tests/test-hyperscript.js | 12 +++++++++--- render/tests/test-normalize.js | 4 ++-- render/tests/test-normalizeChildren.js | 6 ++++++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index f4aad629..4bb0a3b6 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -104,7 +104,7 @@ o.spec("component", function() { visible = false render(root, [{tag: component}]) - o(root.firstChild.nodeValue).equals("false") + o(root.firstChild.nodeValue).equals("") }) o("updates root from null to null", function() { var component = { @@ -232,7 +232,7 @@ o.spec("component", function() { render(root, [{tag: component}]) o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("false") + o(root.firstChild.nodeValue).equals("") }) o("can return null", function() { var component = { @@ -668,7 +668,7 @@ o.spec("component", function() { function init(vnode) { o(vnode.state.data).deepEquals(data) o(vnode.state.data).equals(data) - + //inherits state via prototype component.x = 1 o(vnode.state.x).equals(1) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 8237d0ea..32e767d8 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -229,7 +229,7 @@ o.spec("hyperscript", function() { o("handles falsy boolean single child", function() { var vnode = m("div", {}, [false]) - o(vnode.text).equals(false) + o(vnode.text).equals("") }) o("handles null single child", function() { var vnode = m("div", {}, [null]) @@ -261,7 +261,7 @@ o.spec("hyperscript", function() { var vnode = m("div", {}, [false, true]) o(vnode.children[0].tag).equals("#") - o(vnode.children[0].children).equals(false) + o(vnode.children[0].children).equals("") o(vnode.children[1].tag).equals("#") o(vnode.children[1].children).equals(true) }) @@ -353,10 +353,16 @@ o.spec("hyperscript", function() { o(vnode.text).equals(true) }) o("handles attr and single falsy boolean text child", function() { + var vnode = m("div", {a: "b"}, [0]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals(0) + }) + o("handles attr and single false boolean text child", function() { var vnode = m("div", {a: "b"}, [false]) o(vnode.attrs.a).equals("b") - o(vnode.text).equals(false) + o(vnode.text).equals("") }) o("handles attr and single text child unwrapped", function() { var vnode = m("div", {a: "b"}, "c") diff --git a/render/tests/test-normalize.js b/render/tests/test-normalize.js index 7a01af31..237612a4 100644 --- a/render/tests/test-normalize.js +++ b/render/tests/test-normalize.js @@ -48,10 +48,10 @@ o.spec("normalize", function() { o(node.tag).equals("#") o(node.children).equals(true) }) - o("normalizes falsy boolean into text node", function() { + o("normalizes falsy boolean into empty text node", function() { var node = Vnode.normalize(false) o(node.tag).equals("#") - o(node.children).equals(false) + o(node.children).equals("") }) }) diff --git a/render/tests/test-normalizeChildren.js b/render/tests/test-normalizeChildren.js index 9ad2cd89..699f0eab 100644 --- a/render/tests/test-normalizeChildren.js +++ b/render/tests/test-normalizeChildren.js @@ -16,4 +16,10 @@ o.spec("normalizeChildren", function() { o(children[0].tag).equals("#") o(children[0].children).equals("a") }) + o("normalizes `false` values into empty string text nodes", function() { + var children = Vnode.normalizeChildren([false]) + + o(children[0].tag).equals("#") + o(children[0].children).equals("") + }) }) From a19eae2792cebc87175f09d64940496b0e67f52c Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 6 Jan 2017 16:59:52 +0000 Subject: [PATCH 09/38] Avoid creating intermediary constructor in component state creation --- render/render.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/render/render.js b/render/render.js index 47c65aae..140199ee 100644 --- a/render/render.js +++ b/render/render.js @@ -96,10 +96,7 @@ module.exports = function($window) { } function createComponent(vnode, hooks, ns) { // For object literals since `Vnode()` always sets the `state` field. - if (!vnode.state) vnode.state = {} - var constructor = function() {} - constructor.prototype = vnode.tag - vnode.state = new constructor + if (!vnode.state) vnode.state = Object.create(vnode.tag) var view = vnode.tag.view if (view.reentrantLock != null) return $emptyFragment From e1ea2822c9d95728c31136778c9db202e0f6ef63 Mon Sep 17 00:00:00 2001 From: Barney Carroll Date: Fri, 6 Jan 2017 17:12:56 +0000 Subject: [PATCH 10/38] Fix state initialisation logic --- render/render.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/render/render.js b/render/render.js index 140199ee..4e993f59 100644 --- a/render/render.js +++ b/render/render.js @@ -95,9 +95,7 @@ module.exports = function($window) { return element } function createComponent(vnode, hooks, ns) { - // For object literals since `Vnode()` always sets the `state` field. - if (!vnode.state) vnode.state = Object.create(vnode.tag) - + vnode.state = Object.create(vnode.tag) var view = vnode.tag.view if (view.reentrantLock != null) return $emptyFragment view.reentrantLock = true From 6b15378c41e9a4d56a9941e5928e30e03ac0aed7 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sat, 7 Jan 2017 03:36:49 +0000 Subject: [PATCH 11/38] Bundled output for commit 74ded82165eac505b4a504c33388bd4d59d48a3e [skip ci] --- README.md | 2 +- mithril.js | 6 +--- mithril.min.js | 83 +++++++++++++++++++++++++------------------------- 3 files changed, 43 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 466958b4..b6e99098 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.58 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.59 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 29e9e17c..522c546d 100644 --- a/mithril.js +++ b/mithril.js @@ -430,11 +430,7 @@ var coreRenderer = function($window) { return element } function createComponent(vnode, hooks, ns) { - // For object literals since `Vnode()` always sets the `state` field. - if (!vnode.state) vnode.state = {} - var constructor = function() {} - constructor.prototype = vnode.tag - vnode.state = new constructor + vnode.state = Object.create(vnode.tag) var view = vnode.tag.view if (view.reentrantLock != null) return $emptyFragment view.reentrantLock = true diff --git a/mithril.min.js b/mithril.min.js index 71b834ca..ff483e40 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,42 +1,41 @@ -new function(){function w(b,c,h,d,g,l){return{tag:b,key:c,attrs:h,children:d,text:g,dom:l,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(b){if(null==b||"string"!==typeof b&&null==b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b&&void 0===G[b]){for(var c,h,d=[],g={};c=N.exec(b);){var l=c[1],m=c[2];""===l&&""!==m?h=m:"#"===l?g.id=m:"."===l?d.push(m):"["===c[3][0]&&((l=c[6])&&(l=l.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===c[4]?d.push(l):g[c[4]]=l||!0)}0a.indexOf("?")?"?":"&";a+=d+c}return a}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b);}}function q(b){return b.responseText} -function n(b,c){if("function"===typeof b)if(Array.isArray(c))for(var a=0;ak.status||304===k.status)c(n(a.type,b));else{var g=Error(k.responseText),e;for(e in b)g[e]=b[e];d(g)}}catch(f){d(f)}};h&&null!=a.data?k.send(a.data):k.send()}); -return!0===a.background?z:t(z)},jsonp:function(a,m){var t=h();a=d(a,m);var q=new c(function(c,d){var h=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+k++,m=b.document.createElement("script");b[h]=function(d){m.parentNode.removeChild(m);c(n(a.type,d));delete b[h]};m.onerror=function(){m.parentNode.removeChild(m);d(Error("JSONP request failed"));delete b[h]};null==a.data&&(a.data={});a.url=g(a.url,a.data);a.data[a.callbackKey||"callback"]=h;m.src=l(a.url,a.data);b.document.documentElement.appendChild(m)}); -return!0===a.background?q:t(q)},setCompletionCallback:function(b){t=b}}}(window,x),M=function(b){function c(e,f,b,a,c,d,g){for(;b=u&&z>=t;){var y=f[u],p=b[t];if(y!==p||r)if(null==y)u++;else if(null==p)t++;else if(y.key===p.key)u++,t++,m(e,y,p,d,n(f,u,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g);else if(y=f[v],y!==p||r)if(null==y)v--;else if(null==p)t++;else if(y.key===p.key)m(e, -y,p,d,n(f,v+1,g),r,l),(r||t=u&&z>=t;){y=f[v];p=b[z];if(y!==p||r)if(null==y)v--;else{if(null!=p)if(y.key===p.key)m(e,y,p,d,n(f,v+1,g),r,l),r&&y.tag===p.tag&&k(e,q(y),g),null!=y.dom&&(g=y.dom),v--;else{if(!C){C=f;var y=v,E={},w;for(w=0;wb.indexOf("?")?"?":"&";b+=d+c}return b}function q(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function l(a){return a.responseText} +function t(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b=0;bn.status||304===n.status)c(t(b.type,a));else{var g=Error(n.responseText),e;for(e in a)g[e]=a[e];d(g)}}catch(f){d(f)}};k&&null!=b.data?n.send(b.data):n.send()}); +return!0===b.background?r:z(r)},jsonp:function(b,l){var q=k();b=d(b,l);var z=new c(function(c,d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+p++,l=a.document.createElement("script");a[k]=function(d){l.parentNode.removeChild(l);c(t(b.type,d));delete a[k]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=h(b.url,b.data);b.data[b.callbackKey||"callback"]=k;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)}); +return!0===b.background?z:q(z)},setCompletionCallback:function(a){z=a}}}(window,x),M=function(a){function c(g,e,a,b,c,d,h){for(;a=u&&r>=n;){var y=e[u],v=a[n];if(y!==v||f)if(null==y)u++;else if(null==v)n++;else if(y.key===v.key)u++,n++,m(g,y,v,b,l(e,u,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d);else if(y=e[p],y!==v||f)if(null==y)p--;else if(null==v)n++;else if(y.key===v.key)m(g,y,v,b,l(e,p+1,d),f,h),(f||n=u&&r>=n;){y=e[p];v=a[r];if(y!==v||f)if(null==y)p--;else{if(null!=v)if(y.key===v.key)m(g,y,v,b,l(e,p+1,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d),null!=y.dom&&(d=y.dom),p--;else{if(!A){A=e;var y=p,C={},w;for(w=0;w Date: Sat, 7 Jan 2017 12:04:30 -0500 Subject: [PATCH 12/38] autoredraw docs --- docs/autoredraw.md | 120 +++++++++++++++++++++++++++++++++++++++++++++ docs/guides.md | 1 + 2 files changed, 121 insertions(+) create mode 100644 docs/autoredraw.md diff --git a/docs/autoredraw.md b/docs/autoredraw.md new file mode 100644 index 00000000..ec4c5945 --- /dev/null +++ b/docs/autoredraw.md @@ -0,0 +1,120 @@ +# The auto-redraw system + +Mithril implements a virtual DOM diffing system for fast rendering, and in addition, it offers various mechanisms to gain granular control over the rendering of an application. + +When used idiomatically, Mithril employs an auto-redraw system that synchronizes the DOM whenever changes are made in the data layer. The auto-redraw system becomes enabled when you call `m.mount` or `m.route` (but it stays disabled if your app is bootstrapped solely via `m.render` calls). + +The auto-redraw system simply consists of triggering a re-render function behind the scenes after certain functions complete. + +### After event handlers + +Mithril automatically redraws after DOM event handlers that are defined in a Mithril view: + +```javascript +var MyComponent = { + view: function() { + return m("div", {onclick: doSomething}) + } +} + +function doSomething() { + //a redraw happens synchronously after this function runs +} + +m.mount(document.body, MyComponent) +``` + +You can disable an auto-redraw for specific events by setting `e.redraw` to `false`. + +```javascript +var MyComponent = { + view: function() { + return m("div", {onclick: doSomething}) + } +} + +function doSomething(e) { + e.redraw = false + // no longer triggers a redraw when the div is clicked +} + +m.mount(document.body, MyComponent) +``` + + +### After m.request + +Mithril automatically redraws after [`m.request`](request.md) completes: + +```javascript +m.request("/api/v1/users").then(function() { + //a redraw happens after this function runs +}) +``` + +You can disable an auto-redraw for a specific request by setting the `background` option to true: + +```javascript +m.request("/api/v1/users", {background: true}).then(function() { + //does not trigger a redraw +}) +``` + + +### After route changes + +Mithril automatically redraws after [`m.route.set()`](route.md#routeset) calls (or route changes via links that use [`m.route.link`](route.md#routelink) + +```javascript +var RoutedComponent = { + view: function() { + return [ + // a redraw happens asynchronously after the route changes + m("a", {href: "/", oncreate: m.route.link}), + m("div", { + onclick: function() { + m.route.set("/") + } + }), + ] + } +} + +m.route(document.body, "/", { + "/": RoutedComponent, +}) +``` + +--- + +### When Mithril does not redraws + +Mithril does not redraw after 3rd party library event handlers. In those cases, you must manually call [`m.redraw()`](redraw.md). + +Mithril also does not redraw after lifecycle methods. This is because lifecycle methods run within the redraw cycle and allowing a nested redraw to run could cause loss of stability or even stack overflows. If you need to trigger a redraw within a lifecycle method, you should call `m.redraw` from within the callback of an asynchronous function such as `requestAnimationFrame`, `Promise.resolve` or `setTimeout`. + +```javascript +var StableComponent = { + oncreate: function(vnode) { + vnode.state.height = vnode.dom.offsetHeight + requestAnimationFrame(function() { + m.redraw() + }) + }, + view: function() { + return m("div", "This component is " + vnode.state.height + "px tall") + } +} +``` + +Mithril does not auto-redraw vnode trees that are rendered via `m.render`. This means redraws do not occur after event changes and `m.request` calls for templates that were rendered via `m.render`. Thus, if your architecture requires manual control over when rendering occurs (as can sometimes be the case when using libraries like Redux), you should use `m.render` instead of `m.mount`. + +Remember that `m.render` expects a vnode tree, and `m.mount` expects a component: + +```javascript +// wrap the component in a m() call for m.render +m.render(document.body, m(MyComponent)) + +// don't wrap the component for m.mount +m.mount(document.body, MyComponent) +``` diff --git a/docs/guides.md b/docs/guides.md index d2bd87da..1e05cc3b 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -9,6 +9,7 @@ - [Components](components.md) - [Lifecycle methods](lifecycle-methods.md) - [Keys](keys.md) + - [Autoredraw system](autoredraw.md) - Social - [Community chat](https://gitter.im/lhorie/mithril.js) - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) From 6284a3bd86e3bf126e62c9b9c16dfd8138a9edb5 Mon Sep 17 00:00:00 2001 From: Leo Date: Sat, 7 Jan 2017 14:54:48 -0500 Subject: [PATCH 13/38] es6 and jsx docs --- docs/es6.md | 69 +++++++++++++++++ docs/guides.md | 2 + docs/introduction.md | 9 +-- docs/jsx.md | 177 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 docs/es6.md create mode 100644 docs/jsx.md diff --git a/docs/es6.md b/docs/es6.md new file mode 100644 index 00000000..e8a22000 --- /dev/null +++ b/docs/es6.md @@ -0,0 +1,69 @@ +# ES6 + +Mithril is written in ES5, and is fully compatible with ES6 as well. + +In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like [Babel](https://babeljs.io) is required to compile ES6 features down to ES5. + +### Setup + +The simplest way to setup an ES6 compilation toolchain is via [Babel](https://babeljs.io/). To install, use this command: + +```bash +npm install babel-cli babel-preset-es2015 transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +To run Babel as a standalone tool, run this from the command line: + +```bash +babel src --out-dir lib --source-maps +``` + +#### Using Babel with Webpack + +If you're using Webpack as a bundler, you can integrate Babel to Webpack, however this requires some additional dependencies, in addition to the steps above. + +```bash +npm install babel-core babel-loader --save-dev +``` + +Create a file called `.webpack.config` + +```javascript +module.exports = { + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` + +--- + +### Custom setups + +If you're using Webpack, you can [follow its excellent guide to add support for ES6](https://webpack.github.io/docs/usage.html#transpiling-es2015-using-babel-loader) + +If you want to use Babel as a standalone tool, [here's the instructions for how to set it up](https://babeljs.io/docs/setup/#installation). + +[Google closure compiler](https://www.npmjs.com/package/google-closure-compiler) is another tool that supports ES6 to ES5 compilation. diff --git a/docs/guides.md b/docs/guides.md index 1e05cc3b..1c256266 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -2,6 +2,8 @@ - [Installation](installation.md) - [Introduction](introduction.md) - [Tutorial](simple-application.md) + - [JSX](jsx.md) + - [ES6](es6.md) - [Testing](testing.md) - [Examples](examples.md) - Key concepts diff --git a/docs/introduction.md b/docs/introduction.md index 29c59a4e..b98dfb45 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -25,7 +25,7 @@ Note: This introduction assumes you have basic level of Javacript knowledge. If The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. -Let's create an HTML file to follow along: +Let's create an HTML file to follow along: ```markup @@ -91,7 +91,7 @@ m("main", [ ]) ``` -Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](https://babeljs.io/docs/plugins/transform-react-jsx/). +Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](jsx.md). ```markup // HTML syntax via Babel's JSX plugin @@ -236,8 +236,3 @@ Clicking the button should now update the count. We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR. This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](simple-application.md), which walks you through building a realistic application. - - - - - diff --git a/docs/jsx.md b/docs/jsx.md new file mode 100644 index 00000000..819d8315 --- /dev/null +++ b/docs/jsx.md @@ -0,0 +1,177 @@ +# JSX + +- [Description](#description) +- [Setup](#setup) +- [JSX vs hyperscript](#jsx-vs-hyperscript) +- [Converting HTML](#converting-html) + +--- + +### Description + +JSX is a syntax extension that enables you to write HTML tags interspersed with Javascript. + +``` +var MyComponent = { + view: function() { + return m("main", [ + m("h1", "Hello world"), + ]) + } +} + +// can be written as: +var MyComponent = { + view: function() { + return ( +
+

Hello world

+
+ ) + } +} +``` + +When using JSX, it's possible to interpolate Javascript expressions within JSX tags by using curly braces: + +``` +var greeting = "Hello" +var url = "http://google.com" +var div = {greeting + "!"} +``` + +Components can be used by using a convention of uppercasing the first letter of the component name: + +```javascript +m.mount(document.body, ) +// equivalent to m.mount(document.body, m(Component)) +``` + +--- + +### Setup + +The simplest way to use JSX is via a [Babel](https://babeljs.io/) plugin. To install, use this command: + +```bash +npm install babel-cli babel-preset-es2015 transform-react-jsx --save-dev +``` + +Create a `.babelrc` file: + +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +To run Babel as a standalone tool, run this from the command line: + +```bash +babel src --out-dir lib --source-maps +``` + +#### Using Babel with Webpack + +If you're using Webpack as a bundler, you can integrate Babel to Webpack, however this requires some additional dependencies, in addition to the steps above. + +```bash +npm install babel-core babel-loader --save-dev +``` + +Create a file called `.webpack.config` + +```javascript +module.exports = { + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` + +--- + +### JSX vs hyperscript + +JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and makes syntax issues difficult to spot. + +Unlike HTML, JSX is case-sensitive. This means `
` is different from `
` (all lower case). The former compiles to `m("div", {className: "test"})` and the latter compiles to `m("div", {classname: "test"})`, which is not a valid way of creating a class attribute). Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: `
`. + +JSX is useful for teams where HTML is primarily written by someone without Javascript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser) + +Hyperscript is the compiled representation of JSX. It's designed to be readable and can also be used as-is, instead of JSX (as is done in most of the documentation). Hyperscript tends to be terser than JSX for a couple of reasons: + +1 - it does not require repeating the tag name in closing tags (e.g. `m("div")` vs `
`) +2 - static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs `
` + +In addition, since hyperscript is plain Javascript, it's often more natural to indent than JSX: + +``` +//JSX +var BigComponent = { + activate: function() {/*...*/}, + deactivate: function() {/*...*/}, + update: function() {/*...*/}, + view: function(vnode) { + return [ + {vnode.attrs.items.map(function(item) { + return
{item.name}
+ })} +
+ ] + } +} + +// hyperscript +var BigComponent = { + activate: function() {/*...*/}, + deactivate: function() {/*...*/}, + update: function() {/*...*/}, + view: function(vnode) { + return [ + vnode.attrs.items.map(function(item) { + return m("div", item.name) + }), + m("div", { + ondragover: this.activate, + ondragleave: this.deactivate, + ondragend: this.deactivate, + ondrop: this.update, + onblur: this.deactivate, + }) + ] + } +} +``` + +In non-trivial applications, it's possible for components to have more control flow and component configuration code than markup, making a Javascript-first approach more readable than an HTML-first approach. + +Needless to say, since hyperscript is pure Javascript, there's no need to run a compilation step to produce runnable code. + +--- + +### Converting HTML + +In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX. + +When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can [use the HTML-to-Mithril-template converter](http://arthurclemens.github.io/mithril-template-converter/index.html). From 4b626896fc62f5de0fb352d4dada72242e184044 Mon Sep 17 00:00:00 2001 From: Gandalf-the-Bot Date: Sat, 7 Jan 2017 20:20:45 +0000 Subject: [PATCH 14/38] Bundled output for commit 4fc9368a30b88b3eb485fa7110cf3e2d9c3093db [skip ci] --- README.md | 2 +- mithril.js | 2 +- mithril.min.js | 77 +++++++++++++++++++++++++------------------------- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b6e99098..bfe22f41 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.59 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.60 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/mithril.js b/mithril.js index 522c546d..cbc385e9 100644 --- a/mithril.js +++ b/mithril.js @@ -5,7 +5,7 @@ function Vnode(tag, key, attrs0, children, text, dom) { } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) - if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node, undefined, undefined) + if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined) return node } Vnode.normalizeChildren = function normalizeChildren(children) { diff --git a/mithril.min.js b/mithril.min.js index ff483e40..ad515a32 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,42 @@ new function(){function w(a,c,k,d,h,m){return{tag:a,key:c,attrs:k,children:d,text:h,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function B(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===G[a]){for(var c,k,d=[],h={};c=N.exec(a);){var m=c[1],q=c[2];""===m&&""!==q?k=q:"#"===m?h.id=q:"."===m?d.push(q):"["===c[3][0]&&((m=c[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), "class"===c[4]?d.push(m):h[c[4]]=m||!0)}0b.indexOf("?")?"?":"&";b+=d+c}return b}function q(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a);}}function l(a){return a.responseText} -function t(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b=0;bn.status||304===n.status)c(t(b.type,a));else{var g=Error(n.responseText),e;for(e in a)g[e]=a[e];d(g)}}catch(f){d(f)}};k&&null!=b.data?n.send(b.data):n.send()}); -return!0===b.background?r:z(r)},jsonp:function(b,l){var q=k();b=d(b,l);var z=new c(function(c,d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+p++,l=a.document.createElement("script");a[k]=function(d){l.parentNode.removeChild(l);c(t(b.type,d));delete a[k]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=h(b.url,b.data);b.data[b.callbackKey||"callback"]=k;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)}); -return!0===b.background?z:q(z)},setCompletionCallback:function(a){z=a}}}(window,x),M=function(a){function c(g,e,a,b,c,d,h){for(;a=u&&r>=n;){var y=e[u],v=a[n];if(y!==v||f)if(null==y)u++;else if(null==v)n++;else if(y.key===v.key)u++,n++,m(g,y,v,b,l(e,u,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d);else if(y=e[p],y!==v||f)if(null==y)p--;else if(null==v)n++;else if(y.key===v.key)m(g,y,v,b,l(e,p+1,d),f,h),(f||n=u&&r>=n;){y=e[p];v=a[r];if(y!==v||f)if(null==y)p--;else{if(null!=v)if(y.key===v.key)m(g,y,v,b,l(e,p+1,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d),null!=y.dom&&(d=y.dom),p--;else{if(!A){A=e;var y=p,C={},w;for(w=0;wb.indexOf("?")?"?":"&";b+=d+c}return b}function q(a){try{return""!==a?JSON.parse(a):null}catch(r){throw Error(a); +}}function l(a){return a.responseText}function t(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b=0;bn.status||304===n.status)c(t(b.type,a));else{var g=Error(n.responseText),e;for(e in a)g[e]=a[e]; +d(g)}}catch(f){d(f)}};k&&null!=b.data?n.send(b.data):n.send()});return!0===b.background?r:z(r)},jsonp:function(b,l){var q=k();b=d(b,l);var z=new c(function(c,d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+p++,l=a.document.createElement("script");a[k]=function(d){l.parentNode.removeChild(l);c(t(b.type,d));delete a[k]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=h(b.url,b.data);b.data[b.callbackKey|| +"callback"]=k;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?z:q(z)},setCompletionCallback:function(a){z=a}}}(window,x),M=function(a){function c(g,e,a,b,c,d,h){for(;a=u&&r>=n;){var y=e[u],v=a[n];if(y!==v||f)if(null==y)u++;else if(null==v)n++;else if(y.key===v.key)u++,n++,m(g,y,v,b,l(e,u,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d);else if(y=e[p],y!==v||f)if(null==y)p--;else if(null==v)n++;else if(y.key===v.key)m(g,y,v,b,l(e, +p+1,d),f,h),(f||n=u&&r>=n;){y=e[p];v=a[r];if(y!==v||f)if(null==y)p--;else{if(null!=v)if(y.key===v.key)m(g,y,v,b,l(e,p+1,d),f,h),f&&y.tag===v.tag&&t(g,q(y),d),null!=y.dom&&(d=y.dom),p--;else{if(!A){A=e;var y=p,C={},w;for(w=0;w Date: Sat, 7 Jan 2017 17:56:29 -0500 Subject: [PATCH 15/38] fix typo --- docs/jsx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/jsx.md b/docs/jsx.md index 819d8315..ee6d606d 100644 --- a/docs/jsx.md +++ b/docs/jsx.md @@ -44,7 +44,7 @@ Components can be used by using a convention of uppercasing the first letter of ```javascript m.mount(document.body, ) -// equivalent to m.mount(document.body, m(Component)) +// equivalent to m.mount(document.body, m(MyComponent)) ``` --- From be27f4d5252940343e22c0b38bb9299d70d0e105 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 8 Jan 2017 08:21:33 -0500 Subject: [PATCH 16/38] more documentation tweaks --- docs/generate.js | 1 + docs/guides.md | 1 - docs/installation.md | 7 ++++--- docs/introduction.md | 4 ++-- docs/layout.html | 3 ++- docs/style.css | 3 ++- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/generate.js b/docs/generate.js index f8763522..412d7887 100644 --- a/docs/generate.js +++ b/docs/generate.js @@ -39,6 +39,7 @@ function generate(pathname) { }) .replace(/\.md/gim, ".html") // fix links var html = layout + .replace(/\[version\]/, version) // update version .replace(/\[body\]/, marked(fixed)) .replace(/
([^<]+?)<\/h5>/gim, function(match, id, text) { // fix anchors return "
" + text + "
" diff --git a/docs/guides.md b/docs/guides.md index d2bd87da..4edbcaac 100644 --- a/docs/guides.md +++ b/docs/guides.md @@ -10,7 +10,6 @@ - [Lifecycle methods](lifecycle-methods.md) - [Keys](keys.md) - Social - - [Community chat](https://gitter.im/lhorie/mithril.js) - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) - [How to contribute](contributing.md) - [Credits](credits.md) diff --git a/docs/installation.md b/docs/installation.md index 6993c5d5..4977f120 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -20,16 +20,17 @@ If you're new to Javascript or just want a very simple setup to get your feet we ```bash # 1) install npm install mithril@rewrite --save + npm install webpack --save # 2) add this line into the scripts section in package.json # "scripts": { -# "build": "webpack index.js app.js --watch" +# "build": "webpack src/index.js lib/app.js --watch" # } -# 3) create an `index.js` file +# 3) create an `src/index.js` file -# 4) create an `index.html` file loading `app.js` +# 4) create an `index.html` file containing `` # 5) run bundler npm run build diff --git a/docs/introduction.md b/docs/introduction.md index 29c59a4e..d101d513 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -13,7 +13,7 @@ ### What is Mithril? Mithril is a client-side Javascript framework for building Single Page Applications. -It's small (< 8kb gzip), fast and batteries-included. +It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box. If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](framework-comparison.md) page. @@ -23,7 +23,7 @@ Note: This introduction assumes you have basic level of Javacript knowledge. If ### Getting started -The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. +The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes. Let's create an HTML file to follow along: diff --git a/docs/layout.html b/docs/layout.html index 6e96bb1b..b6e1c504 100644 --- a/docs/layout.html +++ b/docs/layout.html @@ -9,10 +9,11 @@
-

Mithril

+

Mithril [version]

diff --git a/docs/style.css b/docs/style.css index d80b9bca..ae6a7f5d 100644 --- a/docs/style.css +++ b/docs/style.css @@ -10,6 +10,7 @@ h2 {font-size:22px;margin:30px 0 15px;} h3 {font-size:20px;margin:30px 0 15px;} h4 {font-size:18px;margin:15px 0 15px;} h5 {font-weight:bold;margin:15px 0 15px;} +h1 small {font-size:16px;} pre,code {background:#eee;font-family:monospace;} pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;} code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:3px;white-space:pre;} @@ -26,7 +27,7 @@ hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;} #signature + p code {padding:3px 10px;} h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;} h1 + ul + hr {display:none;} -h1 + ul li {border-bottom:1px solid #eee;list-style:none;margin:0;padding:0;} +h1 + ul li {list-style:none;margin:0;padding:0;} h1 + ul li:last-child {border-bottom:0;} h1 + ul ul {margin:0 0 10px;padding:0 0 0 15px;} h1 + ul ul li {border:0;} From 7c00aad19cd7f36e30c8034d240cea7ea7db6c83 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 8 Jan 2017 10:53:54 -0500 Subject: [PATCH 17/38] beef up docs and defrag setup flow --- docs/es6.md | 126 ++++++++++++++++++++++++++++++++----------- docs/hyperscript.md | 9 ++++ docs/installation.md | 63 ++++++++++++++++------ docs/jsx.md | 122 ++++++++++++++++++++++++++++++++--------- 4 files changed, 246 insertions(+), 74 deletions(-) diff --git a/docs/es6.md b/docs/es6.md index e8a22000..3ab05040 100644 --- a/docs/es6.md +++ b/docs/es6.md @@ -1,69 +1,133 @@ # ES6 +- [Setup](#setup) +- [Using Babel with Webpack](#using-babel-with-webpack) + +--- + Mithril is written in ES5, and is fully compatible with ES6 as well. In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like [Babel](https://babeljs.io) is required to compile ES6 features down to ES5. ### Setup -The simplest way to setup an ES6 compilation toolchain is via [Babel](https://babeljs.io/). To install, use this command: +The simplest way to setup an ES6 compilation toolchain is via [Babel](https://babeljs.io/). + +Babel requires NPM, which is automatically installed when you install [Node.js](https://nodejs.org/en/). Once NPM is installed, create a project folder and run this command: ```bash -npm install babel-cli babel-preset-es2015 transform-react-jsx --save-dev +npm init -y +``` + +If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack). + +To install Babel as a standalone tool, use this command: + +```bash +npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev ``` Create a `.babelrc` file: ``` { - "presets": ["es2015"], - "plugins": [ - ["transform-react-jsx", { - "pragma": "m" - }] - ] + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] } ``` To run Babel as a standalone tool, run this from the command line: ```bash -babel src --out-dir lib --source-maps +babel src --out-dir bin --source-maps ``` #### Using Babel with Webpack -If you're using Webpack as a bundler, you can integrate Babel to Webpack, however this requires some additional dependencies, in addition to the steps above. +If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps. ```bash -npm install babel-core babel-loader --save-dev +npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev ``` -Create a file called `.webpack.config` +Create a `.babelrc` file: -```javascript -module.exports = { - entry: './src/index.js', - output: { - path: './bin', - filename: 'app.js', - }, - module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader' - }] - } +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] } ``` ---- +Next, create a file called `webpack.config.js` -### Custom setups +```javascript +module.exports = { + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` -If you're using Webpack, you can [follow its excellent guide to add support for ES6](https://webpack.github.io/docs/usage.html#transpiling-es2015-using-babel-loader) +This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`. -If you want to use Babel as a standalone tool, [here's the instructions for how to set it up](https://babeljs.io/docs/setup/#installation). +To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`: -[Google closure compiler](https://www.npmjs.com/package/google-closure-compiler) is another tool that supports ES6 to ES5 compilation. +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch" + } +} +``` + +You can now then run the bundler by running this from the command line: + +```bash +npm start +``` + +#### Production build + +To generate a minified file, open `package.json` and add a new npm script called `build`: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p" + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } +} +``` diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 6d27a4e6..6108f2d5 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -14,6 +14,7 @@ - [Keys](#keys) - [SVG and MathML](#svg-and-mathml) - [Making templates dynamic](#making-templates-dynamic) +- [Converting HTML](#converting-html) - [Avoid anti-patterns](#avoid-anti-patterns) --- @@ -345,6 +346,14 @@ You cannot use Javascript statements such as `if` or `for` within Javascript exp --- +### Converting HTML + +In Mithril, well-formed HTML is valid JSX. Little effort other than copy-pasting is required to integrate an independently produced HTML file into a project using JSX. + +When using hyperscript, it's necessary to convert HTML to hyperscript syntax before the code can be run. To facilitate this, you can [use the HTML-to-Mithril-template converter](http://arthurclemens.github.io/mithril-template-converter/index.html). + +--- + ### Avoid Anti-patterns Although Mithril is flexible, some code patterns are discouraged: diff --git a/docs/installation.md b/docs/installation.md index 4977f120..ddc85853 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -25,15 +25,15 @@ npm install webpack --save # 2) add this line into the scripts section in package.json # "scripts": { -# "build": "webpack src/index.js lib/app.js --watch" +# "start": "webpack src/index.js bin/app.js --watch" # } # 3) create an `src/index.js` file -# 4) create an `index.html` file containing `` +# 4) create an `index.html` file containing `` # 5) run bundler -npm run build +npm start # 6) open `index.html` in the (default) browser open index.html @@ -52,13 +52,13 @@ npm init --yes # creates a file called package.json ``` -Then, run +Then, to install Mithril, run: ```bash npm install mithril@rewrite --save ``` -to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file +This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules: @@ -75,10 +75,10 @@ CommonJS is a de-facto standard for modularizing Javascript code, and it's used Most browser today do not natively support modularization systems (CommonJS or ES6), so modularized code must be bundled into a single Javascript file before running in a client-side application. -The easiest way to create a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: +A popular way for creating a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: ```bash -npm install webpack --save +npm install webpack --save-dev ``` Open the `package.json` that you created earlier, and add an entry to the `scripts` section: @@ -87,22 +87,24 @@ Open the `package.json` that you created earlier, and add an entry to the `scrip { "name": "my-project", "scripts": { - "build": "webpack index.js app.js --watch" + "start": "webpack src/index.js bin/app.js -d --watch" } } ``` -Remember this is a JSON file, so object key names such as `"scripts"` and `"build"` must be inside of double quotes. +Remember this is a JSON file, so object key names such as `"scripts"` and `"start"` must be inside of double quotes. -Now you can run the script via `npm run build` in your command line window. This looks up the `webpack` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers. - -``` -npm run build -``` +The `-d` flag tells webpack to use development mode, which produces source maps for a better debugging experience. The `--watch` flag tells webpack to watch the file system and automatically recreate `app.js` if file changes are detected. -Now that you have created a bundle, you can then reference the `app.js` file from an HTML file: +Now you can run the script via `npm start` in your command line window. This looks up the `webpack` command in the NPM path, reads `index.js` and creates a file called `app.js` which includes both Mithril and the `hello world` code above. If you want to run the `webpack` command directly from the command line, you need to either add `node_modules/.bin` to your PATH, or install webpack globally via `npm install webpack -g`. It's, however, recommended that you always install webpack locally and use npm scripts, to ensure builds are reproducible in different computers. + +``` +npm start +``` + +Now that you have created a bundle, you can then reference the `bin/app.js` file from an HTML file: ```markup @@ -110,7 +112,7 @@ Now that you have created a bundle, you can then reference the `app.js` file fro Hello world - + ``` @@ -139,6 +141,33 @@ m.mount(document.body, MyComponent) Note that in this example, we're using `m.mount`, which wires up the component to Mithril's autoredraw system. In most applications, you will want to use `m.mount` (or `m.route` if your application has multiple screens) instead of `m.render` to take advantage of the autoredraw system, rather than re-rendering manually every time a change occurs. +#### Production build + +If you open bin/app.js, you'll notice that the Webpack bundle is not minified, so this file is not ideal for a live application. To generate a minified file, open `package.json` and add a new npm script: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack src/index.js bin/app.js -d --watch", + "build": "webpack src/index.js bin/app.js -p", + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } +} +``` + --- ### Alternate ways to use Mithril @@ -167,7 +196,7 @@ The source file `index.js` will be compiled (bundled) and a browser window opens #### Mithril bundler -Mithril comes with a bundler tool of its own. It is sufficient for projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. +Mithril comes with a bundler tool of its own. It is sufficient for ES5-based projects that have no other dependencies other than Mithril, but it's currently considered experimental for projects that require other NPM dependencies. It produces smaller bundles than webpack, but you should not use it in production yet. If you want to try it and give feedback, you can open `package.json` and change the npm script for webpack to this: diff --git a/docs/jsx.md b/docs/jsx.md index ee6d606d..b0c0ff75 100644 --- a/docs/jsx.md +++ b/docs/jsx.md @@ -2,6 +2,7 @@ - [Description](#description) - [Setup](#setup) +- [Using Babel with Webpack](#using-babel-with-webpack) - [JSX vs hyperscript](#jsx-vs-hyperscript) - [Converting HTML](#converting-html) @@ -51,55 +52,124 @@ m.mount(document.body, ) ### Setup -The simplest way to use JSX is via a [Babel](https://babeljs.io/) plugin. To install, use this command: +The simplest way to use JSX is via a [Babel](https://babeljs.io/) plugin. + +Babel requires NPM, which is automatically installed when you install [Node.js](https://nodejs.org/en/). Once NPM is installed, create a project folder and run this command: ```bash -npm install babel-cli babel-preset-es2015 transform-react-jsx --save-dev +npm init -y +``` + +If you want to use Webpack and Babel together, [skip to the section below](#using-babel-with-webpack). + +To install Babel as a standalone tool, use this command: + +```bash +npm install babel-cli babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev ``` Create a `.babelrc` file: ``` { - "presets": ["es2015"], - "plugins": [ - ["transform-react-jsx", { - "pragma": "m" - }] - ] + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] } ``` To run Babel as a standalone tool, run this from the command line: ```bash -babel src --out-dir lib --source-maps +babel src --out-dir bin --source-maps ``` #### Using Babel with Webpack -If you're using Webpack as a bundler, you can integrate Babel to Webpack, however this requires some additional dependencies, in addition to the steps above. +If you're already using Webpack as a bundler, you can integrate Babel to Webpack by following these steps. ```bash -npm install babel-core babel-loader --save-dev +npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx --save-dev ``` -Create a file called `.webpack.config` +Create a `.babelrc` file: + +``` +{ + "presets": ["es2015"], + "plugins": [ + ["transform-react-jsx", { + "pragma": "m" + }] + ] +} +``` + +Next, create a file called `webpack.config.js` ```javascript module.exports = { - entry: './src/index.js', - output: { - path: './bin', - filename: 'app.js', - }, - module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader' - }] - } + entry: './src/index.js', + output: { + path: './bin', + filename: 'app.js', + }, + module: { + loaders: [{ + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }] + } +} +``` + +This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`. + +To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch" + } +} +``` + +You can now then run the bundler by running this from the command line: + +```bash +npm start +``` + +#### Production build + +To generate a minified file, open `package.json` and add a new npm script called `build`: + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + } +} +``` + +You can use hooks in your production environment to run the production build script automatically. Here's an example for [Heroku](https://www.heroku.com/): + +``` +{ + "name": "my-project", + "scripts": { + "start": "webpack -d --watch", + "build": "webpack -p", + "heroku-postbuild": "webpack -p" + } } ``` @@ -107,9 +177,9 @@ module.exports = { ### JSX vs hyperscript -JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and makes syntax issues difficult to spot. +JSX is essentially a trade-off: it introduces a non-standard syntax that cannot be run without appropriate tooling, in order to allow a developer to write HTML code using curly braces. The main benefit of using JSX instead of regular HTML is that the JSX specification is much stricter and yields syntax errors when appropriate, whereas HTML is far too forgiving and can make syntax issues difficult to spot. -Unlike HTML, JSX is case-sensitive. This means `
` is different from `
` (all lower case). The former compiles to `m("div", {className: "test"})` and the latter compiles to `m("div", {classname: "test"})`, which is not a valid way of creating a class attribute). Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: `
`. +Unlike HTML, JSX is case-sensitive. This means `
` is different from `
` (all lower case). The former compiles to `m("div", {className: "test"})` and the latter compiles to `m("div", {classname: "test"})`, which is not a valid way of creating a class attribute. Fortunately, Mithril supports standard HTML attribute names, and thus, this example can be written like regular HTML: `
`. JSX is useful for teams where HTML is primarily written by someone without Javascript experience, but it requires a significant amount of tooling to maintain (whereas plain HTML can, for the most part, simply be opened in a browser) From 3bac29bf78abea2cd7ace1ac59771ef607f465ac Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 8 Jan 2017 11:34:56 -0500 Subject: [PATCH 18/38] Clarify distinctions between RouteResolvers and components --- docs/route.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/route.md b/docs/route.md index 35549125..dde6b993 100644 --- a/docs/route.md +++ b/docs/route.md @@ -109,7 +109,7 @@ Argument | Type | Required | Description #### RouteResolver -A RouterResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. +A RouterResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. A RouteResolver is not a component, and therefore it does NOT have lifecycle methods. As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules. `routeResolver = {onmatch, render}` From 9ad16858a56bc476f3275036c5e0da8f5967c75e Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 8 Jan 2017 13:05:27 -0500 Subject: [PATCH 19/38] #1520 ospec: report duplicate test names --- api/tests/test-router.js | 37 -------------------------- bundler/tests/test-bundler.js | 4 +-- ospec/README.md | 2 +- ospec/ospec.js | 9 +++++++ ospec/package.json | 2 +- promise/tests/test-promise.js | 4 +-- request/tests/test-jsonp.js | 11 -------- test-utils/tests/test-parseURL.js | 2 +- test-utils/tests/test-pushStateMock.js | 6 ----- 9 files changed, 16 insertions(+), 61 deletions(-) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index d70a7c23..6a281237 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1074,43 +1074,6 @@ o.spec("route", function() { }) }) - o("calling route.set invalidates pending onmatch resolution", function(done) { - var rendered = false - var resolved - $window.location.href = prefix + "/a" - route(root, "/a", { - "/a": { - onmatch: function() { - return new Promise(function(resolve) { - callAsync(function() { - callAsync(function() { - resolve({view: function() {}}) - }) - }) - }) - }, - render: function(vnode) { - rendered = true - resolved = "a" - } - }, - "/b": { - view: function() { - resolved = "b" - } - } - }) - - route.set("/b") - - callAsync(function() { - o(rendered).equals(false) - o(resolved).equals("b") - - done() - }) - }) - o("calling route.set invalidates pending onmatch resolution", function(done) { var rendered = false var resolved diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 60859a11..3055bf7c 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -214,14 +214,14 @@ o.spec("bundler", function() { write("c.js", `var cc = 2\nmodule.exports = cc`) bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\nvar x = {}\var bb = 1\nnx.b = bb\nvar cc = 1\nx.c = cc\n}`) + o(read("out.js")).equals(`new function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}`) remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) - o("works if assigned to property", function() { + o("works if assigned to property using bracket notation", function() { write("a.js", `var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")`) write("b.js", `var bb = 1\nmodule.exports = bb`) write("c.js", `var cc = 2\nmodule.exports = cc`) diff --git a/ospec/README.md b/ospec/README.md index 084d949d..ec0173ea 100644 --- a/ospec/README.md +++ b/ospec/README.md @@ -4,7 +4,7 @@ Noiseless testing framework -Version: 1.2.2 +Version: 1.2.3 License: MIT ## About diff --git a/ospec/ospec.js b/ospec/ospec.js index 56111b66..96a09ebe 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -4,6 +4,7 @@ module.exports = new function init() { var spec = {}, subjects = [], results = [], only = null, ctx = spec, start, stack = 0, hasProcess = typeof process === "object" function o(subject, predicate) { + subject = unique(subject, predicate) ctx[subject] = predicate if (predicate === undefined) return new Assert(subject) } @@ -14,6 +15,7 @@ module.exports = new function init() { o.new = init o.spec = function(subject, predicate) { var parent = ctx + subject = unique(subject, predicate) ctx = ctx[subject] = {} predicate() ctx = parent @@ -110,6 +112,13 @@ module.exports = new function init() { } } } + function unique(subject, predicate) { + if (ctx.hasOwnProperty(subject) && predicate) { + console.warn("A test named `" + subject + "` was already defined") + subject = subject + "*" + } + return subject + } function hook(name) { return function(predicate) { if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate) diff --git a/ospec/package.json b/ospec/package.json index 1127ad7f..fdda49db 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -1,6 +1,6 @@ { "name": "ospec", - "version": "1.2.2", + "version": "1.2.3", "description": "Noiseless testing framework", "main": "ospec.js", "directories": { diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js index aa3a1bd9..a91b0bcb 100644 --- a/promise/tests/test-promise.js +++ b/promise/tests/test-promise.js @@ -27,7 +27,7 @@ o.spec("promise", function() { o(promise instanceof Promise).equals(true) }) - o("static resolve returns promise", function() { + o("static reject returns promise", function() { var promise = Promise.reject() promise.catch(function() {}) @@ -141,7 +141,7 @@ o.spec("promise", function() { done() }) }) - o("non-function onFulfilled is ignored", function(done) { + o("non-function onFulfilled with no second param is ignored", function(done) { var promise = Promise.resolve(1) promise.then(null).then(function(value) { diff --git a/request/tests/test-jsonp.js b/request/tests/test-jsonp.js index 6dd0a619..7042274c 100644 --- a/request/tests/test-jsonp.js +++ b/request/tests/test-jsonp.js @@ -64,17 +64,6 @@ o.spec("jsonp", function() { o(data).deepEquals({a: 2}) }).then(done) }) - o("works w/ custom callbackKey", function(done) { - mock.$defineRoutes({ - "GET /item": function(request) { - var queryData = parseQueryString(request.query) - return {status: 200, responseText: queryData["cb"] + "(" + JSON.stringify({a: 2}) + ")"} - } - }) - jsonp({url: "/item", callbackKey: "cb"}).then(function(data) { - o(data).deepEquals({a: 2}) - }).then(done) - }) o("requests don't block each other", function(done) { mock.$defineRoutes({ "GET /item": function(request) { diff --git a/test-utils/tests/test-parseURL.js b/test-utils/tests/test-parseURL.js index 10bb7e81..b722cd8d 100644 --- a/test-utils/tests/test-parseURL.js +++ b/test-utils/tests/test-parseURL.js @@ -136,7 +136,7 @@ o.spec("parseURL", function() { o(data.search).equals("?a/c") o(data.hash).equals("") }) - o("handles search w/ slash", function() { + o("handles search w/ colon", function() { var data = parseURL("http://www.google.com/test?a:c") o(data.pathname).equals("/test") o(data.search).equals("?a:c") diff --git a/test-utils/tests/test-pushStateMock.js b/test-utils/tests/test-pushStateMock.js index 86298b30..3cb51716 100644 --- a/test-utils/tests/test-pushStateMock.js +++ b/test-utils/tests/test-pushStateMock.js @@ -382,12 +382,6 @@ o.spec("pushStateMock", function() { o($window.onpopstate.callCount).equals(2) }) - o("history.back() without history does not trigger onpopstate", function() { - $window.onpopstate = o.spy() - $window.history.back() - - o($window.onpopstate.callCount).equals(0) - }) o("history.forward() without history does not trigger onpopstate", function() { $window.onpopstate = o.spy() $window.history.forward() From 82ebff4336d65f5b7dcc501e82aa94a15a9e1fa4 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 8 Jan 2017 13:35:56 -0500 Subject: [PATCH 20/38] tut --- docs/simple-application.md | 605 +++++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 docs/simple-application.md diff --git a/docs/simple-application.md b/docs/simple-application.md new file mode 100644 index 00000000..6e00e20e --- /dev/null +++ b/docs/simple-application.md @@ -0,0 +1,605 @@ +# Simple application + +Let's develop a simple application that covers some of the major aspects of Single Page Applications + +First let's create an entry point for the application. Create a file `index.html`: + +```markup + + + + + + My Application + + + + + +``` + +The `` line indicates this is an HTML 5 document. The first `charset` meta tag indicates the encoding of the document and the `viewport` meta tag dictates how mobile browsers should scale the page. The `title` tag contains the text to be displayed on the browser tab for this application, and the `script` tag indicates what is the path to the Javascript file that controls the application. + +We could create the entire application in a single Javascript file, but doing so would make it difficult to navigate the codebase later on. Instead, let's split the code into *modules*, and assemble these modules into a *bundle* `app.js`. + +There are many ways to setup a bundler tool, but most are distributed via NPM. In fact, most modern Javascript libraries and tools are distributed that way, including Mithril. NPM stands for Node.js Package Manager. To download NPM, [install Node.js](https://nodejs.org/en/); NPM is installed automatically with it. Once you have Node.js and NPM installed, open the command line and run this command: + +```bash +npm init -y +``` + +If NPM is installed correctly, a file `package.json` will be created. This file will contain a skeleton project meta-description file. Feel free to edit the project and author information in this file. + +--- + +To install Mithril, follow the instructions in the [installation](installation.md) page. Once you have a project skeleton with Mithril installed, we are ready to create the application. + +Let's start by creating a module to store our state. Let's create a file called `src/models/User.js` + +```javascript +// src/models/User.js +var User = { + list: [] +} + +module.exports = User +``` + +Now let's add code to load some data from a server. To communicate with a server, we can use Mithril's XHR utility, `m.request`. First, we include Mithril in the module: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [] +} + +module.exports = User +``` + +Next we create a function that will trigger an XHR call. Let's call it `loadList` + +```javascript +// models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + // TODO: make XHR call + } +} + +module.exports = User +``` + +Then we can add an `m.request` call to make an XHR request. For this tutorial, we'll make XHR calls to the [REM](http://rem-rest-api.herokuapp.com/) API, a mock REST API designed for rapid prototyping. This API returns a list of users from the `GET http://rem-rest-api.herokuapp.com/api/users` endpoint. Let's use `m.request` to make an XHR request and populate our data with the response of that endpoint. + +```javascript +// models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, +} + +module.exports = User +``` + +The `method` option is an [HTTP method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods). To retrieve data from the server without causing side-effects on the server, we need to use the `GET` method. The `url` is the address for the API endpoint. The `withCredentials: true` line indicates that we're using cookies (which is a requirement for the REM API). + +The `m.request` call returns a Promise that resolves to the data from the endpoint. By default, Mithril assumes a HTTP response body are in JSON format and automatically parses it into a Javascript object or array. The `.then` callback runs when the XHR request completes. In this case, the callback assigns the `reesult.data` array to `User.list`. + +Notice we also have a `return` statement in `loadList`. This is a general good practice when working with Promises, which allows us to register more callbacks to run after the completion of the XHR request. + +This simple model exposes two members: `User.list` (an array of user objects), and `User.loadList` (a method that populates `User.list` with server data). + +--- + +Now, let's create a view module so that we can display data from our User model module. + +Create a file called `src/views/UserList.js`. First, let's include Mithril and our model, since we'll need to use both: + +```javascript +var m = require("mithril") +var User = require("../model/User") +``` + +Next, let's create a Mithril component. A component is simply an object that has a `view` method: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + // TODO add code here + } +} +``` + +By default, Mithril views are described using [hyperscript](hyperscript.md). Hyperscript offers a terse syntax that can be indented more naturally than HTML for complex tags, and in addition, since its syntax is simply Javascript, it's possible to leverage a lot of Javascript tooling ecosystem: for example [Babel](es6.md), [JSX](jsx.md) (inline-HTML syntax extension), [eslint](http://eslint.org/) (linting), [uglifyjs](https://github.com/mishoo/UglifyJS2) (minification), [istanbul](https://github.com/gotwarlost/istanbul) (code coverage), [flow](https://flowtype.org/) (static type analysis), etc. + +Let's use Mithril hyperscript to create a list of items. Hyperscript is the most idiomatic way of writing Mithril views, but [JSX is another popular alternative that you could explore](jsx.md) once you're more comfortable with the basics: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + return m(".user-list") + } +} +``` + +The `".user-list"` string is a CSS selector, and as you would expect, `.user-list` represents a class. When a tag is not specified, `div` is the default. So this view is equivalent to `
`. + +Now, let's reference the list of users from the model we created earlier (`User.list`) to dynamically loop through data: + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m(".user-list-item", user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Since `User.list` is a Javascript array, and since hyperscript views are just Javascript, we can loop through the array using the `.map` method. This creates an array of vnodes that represents a list of `div`s, each containing the name of a user. + +The problem, of course, is that we never called the `User.loadList` function. Therefore, `User.list` is still an empty array, and thus this view would render a blank page. Since we want `User.loadList` to be called when we render this component, we can take advantage of component [lifecycle methods](lifecycle-methods.md): + +```javascript +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + oninit: User.loadList, + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m(".user-list-item", user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Notice that we added an `oninit` method to the component, which references `User.loadList`. This means that when the component initializes, User.loadList will be called, triggering an XHR request. When the server returns a response, `User.list` gets populated. + +Also notice we **didn't** do `oninit: User.loadList()` (with parentheses at the end). The difference is that `oninit: User.loadList()` calls the function once and immediately, but `oninit: User.loadList` only calls that function when the component renders. This is an important difference and a common newbie mistake: calling the function immediately means that the XHR request will fire even if the component never renders. Also, if the component is ever recreated (through navigating back and forth through the application), the function won't be called again as expected. + +--- + +Let's render the view from the entry point file `index.js` we created earlier: + +```javascript +var m = require("mithril") + +var UserList = require("./view/UserList") + +m.mount(document.body, UserList) +``` + +The `m.mount` call renders the specified component (`UserList`) into a DOM element (`document.body`), erasing any DOM that were there previously. Opening the HTML file in a browser should now display a list of person names. + +--- + +Right now, the list looks rather plain because we have not defined any styles. + +There are many similar conventions and libraries that help organize application styles nowadays. Some, like [Bootstrap](http://getbootstrap.com/) dictate a specific set of HTML structures and semantically meaningful class names, which has the upside of providing low cognitive dissonance, but the downside of making customization more difficult. Others, like [Tachyons](http://tachyons.io/) provide a large number of self-describing, atomic class names at the cost of making the class names themselves non-semantic. "CSS-in-JS" is another type of CSS system that is growing in popularity, which basically consists of scoping CSS via transpilation tooling. CSS-in-JS libraries achieve maintainability by reducing the size of the problem space, but come at the cost of having high complexity. + +Regardless of what CSS convention/library you choose, a good rule of thumb is to avoid the cascading aspect of CSS. To keep this tutorial simple, we'll just use plain CSS with overly explicit class names, so that the styles themselves provide the atomicity of Tachyons, and class name collisions are made unlikely through the verbosity of the class names. Plain CSS can be sufficient for low-complexity projects (e.g. 3 to 6 man-months of initial implementation time and few project phases) + +To add styles, let's first create a file called `styles.css` and include it in the `index.html` file + +```markup + + + + + + My Application + + + + + + +``` + +Now we can style the `UserList` component: + +```css +.user-list {list-style:none;margin:0 0 10px;padding:0;} +.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;} +.user-list-item:hover {text-decoration:underline;} +``` + +The CSS above is written using a convention of keeping all styles for a rule in a single line, in alphabetical order. This convention is designed to take maximum advantage of screen real estate, and makes it easier to scan the CSS selectors (since they are always on the left side) and their logical grouping, and it enforces predictable and uniform placement of CSS rules for each selector. + +Obviously you can use whatever spacing/indentation convention you prefer. The example above is just an illustration of a not-so-widespread convention that has strong rationales behind it, but deviate from the more widespread cosmetic-oriented spacing conventions. + +Reloading the browser window now should display some styled elements. + +--- + +Let's add routing to our application. + +Routing means binding a screen to a unique URL, to create the ability to go from one "page" to another. Mithril is designed for Single Page Applications, so these "pages" aren't necessarily different HTML files in the traditional sense of the word. Instead, routing in Single Page Applications retains the same HTML file throughout its lifetime, but changes the state of the application via Javascript. Client side routing has the benefit of avoiding flashes of blank screen between page transitions, and can reduce the amount of data being sent down from the server when used in conjunction with an web service oriented architecture (i.e. an application that downloads data as JSON instead of downloading pre-rendered chunks of verbose HTML). + +We can add routing by changing the `m.mount` call to a `m.route` call: + +```javascript +var m = require("mithril") + +var UserList = require("./view/UserList") + +m.route(document.body, "/list", { + "/list": UserList +}) +``` + +The `m.route` call specifies that the application will be rendered into `document.body`. The `"/list"` argument is the default route. That means the user will be redirected to that route if they land in a route that does not exist. The `{"/list": UserList}` object declares a map of existing routes, and what components each route resolves to. + +Refreshing the page in the browser should now append `#!/list` to the URL to indicate that routing is working. Since that route render UserList, we should still see the list of people on screen as before. + +The `#!` snippet is known as a hashbang, and it's a commonly used string for implementing client-side routing. It's possible to configure this string it via [`m.route.prefix`](route.md#routeprefix). Some configurations require supporting server-side changes, so we'll just continue using the hashbang for the rest of this tutorial. + +--- + +Let's add another route to our application for editing users. First let's create a module called `views/UserForm.js` + +```javascript +// src/views/UserForm.js + +module.exports = { + view: function() { + // TODO implement view + } +} +``` + +Then we can `require` this new module from `index.js` + +```javascript +// index.js +var m = require("mithril") + +var UserList = require("./view/UserList") +var UserForm = require("./view/UserForm") + +m.route(document.body, "/list", { + "/list": UserList +}) +``` + +And finally, we can create a route that references it: + +```javascript +// index.js +var m = require("mithril") + +var UserList = require("./view/UserList") +var UserForm = require("./view/UserForm") + +m.route(document.body, "/list", { + "/list": UserList, + "/edit/:id": UserForm, +}) +``` + +Notice that the new route has a `:id` in it. This is a route parameter; you can think of it as a wild card; the route `/edit/1` would resolve to `UserForm` with an `id` of `"1"`. `/edit/2` would also resolve to `UserForm`, but with an `id` of `"2"`. And so on. + +Let's implement the `UserForm` component so that it can respond to those route parameters: + +```javascript +// src/views/UserForm.js +var m = require("mithril") + +module.exports = { + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]"), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]"), + m("button.button[type=submit]", "Save"), + ]) + } +} +``` + +And let's add some styles to `styles.css`: + +```css +/* styles.css */ +body,.input,.button {font:normal 16px Verdana;margin:0;} + +.user-list {list-style:none;margin:0 0 10px;padding:0;} +.user-list-item {background:#fafafa;border:1px solid #ddd;color:#333;display:block;margin:0 0 1px;padding:8px 15px;text-decoration:none;} +.user-list-item:hover {text-decoration:underline;} + +.label {display:block;margin:0 0 5px;} +.input {border:1px solid #ddd;border-radius:3px;box-sizing:border-box;display:block;margin:0 0 10px;padding:10px 15px;width:100%;} +.button {background:#eee;border:1px solid #ddd;border-radius:3px;color:#333;display:inline-block;margin:0 0 10px;padding:10px 15px;text-decoration:none;} +.button:hover {background:#e8e8e8;} +``` + +Right now, this component does nothing to respond to user events. Let's add some code to our `User` model in `src/model/User.js`. This is how the code is right now: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, +} + +module.exports = User +``` + +Let's add code to allow us to load a single user + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, + + current: {}, + load: function(id) { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: {id: id}, + withCredentials: true, + }) + .then(function(result) { + User.current = result + }) + } +} + +module.exports = User +``` + +Notice we added a `User.current` property, and a `User.load(id)` method which populates that property. We can now populate the `UserForm` view using this new method: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("./model/User") + +module.exports = { + oninit: function(vnode) {User.load(vnode.attrs.id)}, + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]", {value: User.current.firstName}), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]", {value: User.current.lastName}), + m("button.button[type=submit]", "Save"), + ]) + } +} +``` + +Similar to the `UserList` component, `oninit` calls `User.load()`. Remember we had a route parameter called `:id` on the `"/edit/:id": UserForm` route? The route parameter becomes an attribute of the `UserForm` component's vnode, so routing to `/edit/1` would make `vnode.attrs.id` have a value of `"1"`. + +Now, let's modify the `UserList` view so that we can navigate from there to a `UserForm`: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("../model/User") + +module.exports = { + oninit: User.loadList, + view: function() { + return m(".user-list", [ + User.list.map(function(user) { + return m("a.user-list-item", {href: "/edit/" + user.id, oncreate: m.route.link}, user.firstName + " " + user.lastName) + }) + ]) + } +} +``` + +Here we changed `.user-list-item` to `a.user-list-item`. We added an `href` that references the route we want, and finally we added `oncreate: m.route.link`. This makes the link behave like a routed link (as opposed to merely behaving like a regular link). What this means is that clicking the link would change the part of URL that comes after the hashbang `#!` (thus changing the route without unloading the current HTML page) + +If you refresh the page in the browser, you should now be able to click on a person and be taken to a form. You should also be able to press the back button in the browser to go back from the form to the list of people. + +--- + +The form itself still doesn't save when you press "Save". Let's make this form work: + +```javascript +// src/views/UserForm.js +var m = require("mithril") +var User = require("./model/User") + +module.exports = { + oninit: function(vnode) {User.load(vnode.attrs.id)}, + view: function() { + return m("form", [ + m("label.label", "First name"), + m("input.input[type=text][placeholder=First name]", { + oninput: m.withAttr("value", function(value) {User.current.firstName = value}), + value: User.current.firstName + }), + m("label.label", "Last name"), + m("input.input[placeholder=Last name]", { + oninput: m.withAttr("value", function(value) {User.current.lastName = value}), + value: User.current.lastName + }), + m("button.button[type=submit]", {onclick: User.save}, "Save"), + ]) + } +} +``` + +We added `oninput` events to both inputs, that set the `User.current.firstName` and `User.current.lastName` properties when a user types. + +In addition, we declared that a `User.save` method should be called when the "Save" button is pressed. Let's implement that method: + +```javascript +// src/models/User.js +var m = require("mithril") + +var User = { + list: [], + loadList: function() { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users", + withCredentials: true, + }) + .then(function(result) { + User.list = result.data + }) + }, + + current: {}, + load: function(id) { + return m.request({ + method: "GET", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: {id: id}, + withCredentials: true, + }) + .then(function(result) { + User.current = result + }) + }, + + save: function() { + return m.request({ + method: "PUT", + url: "http://rem-rest-api.herokuapp.com/api/users/:id", + data: User.current, + withCredentials: true, + }) + } +} + +module.exports = User +``` + +In the `save` method at the bottom, we used the `PUT` HTTP method to indicate that we are upserting data to the server. + +Now try editing the name of a user in the application. Once you save a change, you should be able to see the change reflected in the list of users. + +--- + +Currently, we're only able to navigate back to the user list via the browser back button. Ideally, we would like to have a menu - or more generically, a layout where we can put global UI elements + +Let's create a file `src/views/Layout.js`: + +```javascript +var Layout = { + view: function(vnode) { + return m("main.layout", [ + m("nav.menu", [ + m("a[href='/list']", {oncreate: m.route.link}, "Users") + ]), + m("section", vnode.children) + ]) + } +} +``` + +This component is fairly straightforward, it has a `