diff --git a/api/router.js b/api/router.js index 78c42375..496b5684 100644 --- a/api/router.js +++ b/api/router.js @@ -7,16 +7,16 @@ var autoredraw = require("../api/autoredraw") module.exports = function($window, renderer, pubsub) { var router = coreRouter($window) var route = function(root, defaultRoute, routes) { - var current = {route: null, component: null} + var current = {path: null, component: null} var replay = router.defineRoutes(routes, function(payload, args, path, route) { if (typeof payload.view !== "function") { if (typeof payload.render !== "function") payload.render = function(vnode) {return vnode} var render = function(component) { - current.route = route, current.component = component + current.path = path, current.component = component renderer.render(root, payload.render(Vnode(component, null, args, undefined, undefined, undefined))) } if (typeof payload.resolve !== "function") payload.resolve = function() {render(current.component)} - if (route !== current.route) payload.resolve(render, args, path, route) + if (path !== current.path) payload.resolve(render, args, path, route) else render(current.component) } else { diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index 5bc39e5f..24a9bc78 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -68,6 +68,71 @@ o.spec("mount", function() { }, FRAME_BUDGET) }) + o("redraws several mount points on events", function(done) { + var onupdate0 = o.spy() + var oninit0 = o.spy() + var onclick0 = o.spy() + var onupdate1 = o.spy() + var oninit1 = o.spy() + var onclick1 = o.spy() + + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + render(root, [ + m("#child0"), + m("#child1") + ]) + + mount(root.childNodes[0], { + view : function() { + return m("div", { + oninit : oninit0, + onupdate : onupdate0, + onclick : onclick0, + }) + } + }) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + mount(root.childNodes[1], { + view : function() { + return m("div", { + oninit : oninit1, + onupdate : onupdate1, + onclick : onclick1, + }) + } + }) + + o(oninit1.callCount).equals(1) + o(onupdate1.callCount).equals(0) + + root.childNodes[0].firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root.childNodes[0].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) + + root.childNodes[1].firstChild.dispatchEvent(e) + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root.childNodes[1].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + + }) + o("event handlers can skip redraw", function(done) { var onupdate = o.spy() var oninit = o.spy() diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 627ffc34..713f9bf1 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -11,325 +11,327 @@ var apiPubSub = require("../../api/pubsub") var apiRouter = require("../../api/router") o.spec("route", function() { - void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "`", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redraw, route + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { + void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { + o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { + var FRAME_BUDGET = Math.floor(1000 / 60) + var $window, root, redraw, route - o.beforeEach(function() { - $window = {} + o.beforeEach(function() { + $window = {} - var dom = domMock() - for (var key in dom) $window[key] = dom[key] + var dom = domMock() + for (var key in dom) $window[key] = dom[key] - var loc = pushStateMock() - for (var key in loc) $window[key] = loc[key] + var loc = pushStateMock(env) + for (var key in loc) $window[key] = loc[key] - root = $window.document.body + root = $window.document.body - redraw = apiPubSub() - route = apiRouter($window, coreRenderer($window), redraw) - route.prefix(prefix) - }) + redraw = apiPubSub() + route = apiRouter($window, coreRenderer($window), redraw) + route.prefix(prefix) + }) - o("renders into `root`", function(done) { - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: function() { - return m("div") + o("renders into `root`", function(done) { + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + view: function() { + return m("div") + } } - } - }) - - callAsync(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }) - }) - - o("default route doesn't break back button", function(done) { - $window.location.href = "http://google.com" - route(root, "/a", { - "/a" : { - view: function() { - return m("div") - } - } - }) - - setTimeout(function() { - o(root.firstChild.nodeName).equals("DIV") - - $window.history.back() - - o($window.location.pathname).equals("/") - - done() - }, FRAME_BUDGET) - }) - - o("default route does not inherit params", function(done) { - $window.location.href = "/invalid?foo=bar" - route(root, "/a", { - "/a" : { - oninit: init, - view: function() { - return m("div") - } - } - }) - - function init(vnode) { - o(vnode.attrs).deepEquals({}) - - done() - } - }) - - o("redraws when render function is executed", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - } - }) - - callAsync(function() { - o(oninit.callCount).equals(1) - - redraw.publish() - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) + }) + callAsync(function() { + o(root.firstChild.nodeName).equals("DIV") + done() - }, FRAME_BUDGET) + }) }) - }) - o("redraws on events", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: onclick, - }) + o("default route doesn't break back button", function(done) { + $window.location.href = "http://google.com" + route(root, "/a", { + "/a" : { + view: function() { + return m("div") + } } - } - }) - - callAsync(function() { - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - done() - }, FRAME_BUDGET) - }) - }) - - o("event handlers can skip redraw", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false - }, - }) - } - } - }) - - callAsync(function() { - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - // Wrapped to ensure no redraw fired - setTimeout(function() { - o(onupdate.callCount).equals(0) - - done() - }, FRAME_BUDGET) - }) - }) - - o("changes location on route.link", function(done) { - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - view: function() { - return m("a", { - href: "/test", - oncreate: route.link - }) - } - }, - "/test" : { - view : function() { - return m("div") - } - } - }) - - callAsync(function() { - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals("http://localhost" + slash + (prefix ? prefix + "/" : "")) - - root.firstChild.dispatchEvent(e) - - o($window.location.href).equals("http://localhost" + slash + (prefix ? prefix + "/" : "") + "test") - - done() - }) - }) - - o("accepts object as payload", function(done) { - var Component = { - view: function() { - return m("div") - } - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - resolve: function(resolve) {resolve(Component)}, - render: function(vnode) {return vnode}, - }, - }) - - callAsync(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }) - }) - - o("accepts object without `render` method as payload", function(done) { - var Component = { - view: function() { - return m("div") - } - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - resolve: function(resolve) {resolve(Component)}, - }, - }) - - callAsync(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }) - }) - - o("accepts object without `resolve` method as payload", function(done) { - var Component = { - view: function() { - return m("div") - } - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - render: function() {return m(Component)}, - }, - }) - - callAsync(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }) - }) - - o("calls resolve and render correct number of times", function(done) { - var resolveCount = 0 - var renderCount = 0 - var Component = { - view: function() { - return m("div") - } - } - - $window.location.href = prefix + "/" - route(root, "/", { - "/" : { - resolve: function(resolve) { - resolveCount++ - resolve(Component) - }, - render: function(vnode) { - renderCount++ - return vnode - }, - }, - }) - - callAsync(function() { - o(resolveCount).equals(1) - o(renderCount).equals(1) - - redraw.publish() + }) setTimeout(function() { - o(resolveCount).equals(1) - o(renderCount).equals(2) + o(root.firstChild.nodeName).equals("DIV") + + $window.history.back() + + o($window.location.pathname).equals("/") done() }, FRAME_BUDGET) }) + + o("default route does not inherit params", function(done) { + $window.location.href = "/invalid?foo=bar" + route(root, "/a", { + "/a" : { + oninit: init, + view: function() { + return m("div") + } + } + }) + + function init(vnode) { + o(vnode.attrs).deepEquals({}) + + done() + } + }) + + o("redraws when render function is executed", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate + }) + } + } + }) + + callAsync(function() { + o(oninit.callCount).equals(1) + + redraw.publish() + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + }) + + o("redraws on events", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate, + onclick: onclick, + }) + } + } + }) + + callAsync(function() { + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + }) + + o("event handlers can skip redraw", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate, + onclick: function(e) { + e.redraw = false + }, + }) + } + } + }) + + callAsync(function() { + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + // Wrapped to ensure no redraw fired + setTimeout(function() { + o(onupdate.callCount).equals(0) + + done() + }, FRAME_BUDGET) + }) + }) + + o("changes location on route.link", function(done) { + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + view: function() { + return m("a", { + href: "/test", + oncreate: route.link + }) + } + }, + "/test" : { + view : function() { + return m("div") + } + } + }) + + callAsync(function() { + var slash = prefix[0] === "/" ? "" : "/" + + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) + + root.firstChild.dispatchEvent(e) + + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") + + done() + }) + }) + + o("accepts object as payload", function(done) { + var Component = { + view: function() { + return m("div") + } + } + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + resolve: function(resolve) {resolve(Component)}, + render: function(vnode) {return vnode}, + }, + }) + + callAsync(function() { + o(root.firstChild.nodeName).equals("DIV") + + done() + }) + }) + + o("accepts object without `render` method as payload", function(done) { + var Component = { + view: function() { + return m("div") + } + } + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + resolve: function(resolve) {resolve(Component)}, + }, + }) + + callAsync(function() { + o(root.firstChild.nodeName).equals("DIV") + + done() + }) + }) + + o("accepts object without `resolve` method as payload", function(done) { + var Component = { + view: function() { + return m("div") + } + } + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + render: function() {return m(Component)}, + }, + }) + + callAsync(function() { + o(root.firstChild.nodeName).equals("DIV") + + done() + }) + }) + + o("calls resolve and render correct number of times", function(done) { + var resolveCount = 0 + var renderCount = 0 + var Component = { + view: function() { + return m("div") + } + } + + $window.location.href = prefix + "/" + route(root, "/", { + "/" : { + resolve: function(resolve) { + resolveCount++ + resolve(Component) + }, + render: function(vnode) { + renderCount++ + return vnode + }, + }, + }) + + callAsync(function() { + o(resolveCount).equals(1) + o(renderCount).equals(1) + + redraw.publish() + + setTimeout(function() { + o(resolveCount).equals(1) + o(renderCount).equals(2) + + done() + }, FRAME_BUDGET) + }) + }) }) }) }) diff --git a/browser.js b/browser.js index 6c39538a..fed323f1 100644 --- a/browser.js +++ b/browser.js @@ -1,3 +1,3 @@ -;(function() { -window.m = require("./index") -})() \ No newline at end of file +var m = require("./index") +if (typeof module !== "undefined") module["exports"] = m +else window.m = m diff --git a/docs/hyperscript.md b/docs/hyperscript.md index 27e89e8f..e8c392a4 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -23,7 +23,7 @@ Argument | Type | Required | Description ------------ | ------------------------------------------ | -------- | --- -`selector` | `String|Object` | Yes | A CSS selector or a component +`selector` | `String|Object` | Yes | A CSS selector or a [component](https://github.com/lhorie/mithril.js/blob/rewrite/docs/components.md) `attributes` | `Object` | No | HTML attributes or element properties `children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats) **returns** | `Vnode` | | A [vnode](vnodes.md#structure) diff --git a/mithril.js b/mithril.js index e9c04d15..d80f0169 100644 --- a/mithril.js +++ b/mithril.js @@ -1,5 +1,4 @@ new function() { -"use strict" var guid = 0, noop = function() {}, HALT = {} function createStream() { function stream() { @@ -944,7 +943,7 @@ var parseQueryString = function(string) { return data } var coreRouter = function($window) { - var supportsPushState = typeof $window.history.pushState === "function" && $window.location.protocol !== "file:" + var supportsPushState = typeof $window.history.pushState === "function" var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout var prefix = "#!" function setPrefix(value) {prefix = value} @@ -1006,6 +1005,7 @@ var coreRouter = function($window) { var path = getPath() var params = {} var pathname = parsePath(path, params, params) + callAsync(function() { for (var route in routes) { var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") @@ -1072,16 +1072,16 @@ var autoredraw = function(root, renderer, pubsub, callback) { m.route = function($window, renderer, pubsub) { var router = coreRouter($window) var route = function(root, defaultRoute, routes) { - var current = {route: null, component: null} + var current = {path: null, component: null} var replay = router.defineRoutes(routes, function(payload, args, path, route) { if (typeof payload.view !== "function") { if (typeof payload.render !== "function") payload.render = function(vnode) {return vnode} var render = function(component) { - current.route = route, current.component = component + current.path = path, current.component = component renderer.render(root, payload.render(Vnode(component, null, args, undefined, undefined, undefined))) } if (typeof payload.resolve !== "function") payload.resolve = function() {render(current.component)} - if (route !== current.route) payload.resolve(render, args, path, route) + if (path !== current.path) payload.resolve(render, args, path, route) else render(current.component) } else { @@ -1125,6 +1125,7 @@ m.redraw = redrawService.publish m.request = requestService.xhr m.jsonp = requestService.jsonp m.version = "1.0.0" -module.exports = m +if (typeof module !== "undefined") module["exports"] = m +else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index e687f9a6..3b2074e8 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -15,26 +15,26 @@ f)if(null==c)b(a,f,0,f.length,d,g,void 0);else if(null==f)v(a,c,0,c.length,f);el c[q]?n(a,h(f[q],d,B),e(c,q+1,g)):null==f[q]?v(a,c,q,q+1,f):k(a,c[q],f[q],d,e(c,q+1,g),p,B),p&&c[q].tag===f[q].tag&&n(a,m(c[q]),e(c,q+1,g))}else{for(var A=q=0,r=c.length-1,z=f.length-1,u;r>=q&&z>=A;){var t=c[q],w=f[A];if(t===w)q++,A++;else if(null!=t&&null!=w&&t.key===w.key)q++,A++,k(a,t,w,d,e(c,q,g),p,B),p&&t.tag===w.tag&&n(a,m(t),g);else if(t=c[r],t===w)r--,A++;else if(null!=t&&null!=w&&t.key===w.key)k(a,t,w,d,e(c,r+1,g),p,B),n(a,m(t),e(c,q,g)),r--,A++;else break}for(;r>=q&&z>=A;){t=c[r];w=f[z]; if(t===w)r--;else if(null!=t&&null!=w&&t.key===w.key)k(a,t,w,d,e(c,r+1,g),p,B),p&&t.tag===w.tag&&n(a,m(t),g),null!=t.dom&&(g=t.dom),r--;else{if(!u){u=c;var t=r,l={},x;for(x=0;xa.indexOf("?")?"?":"&";a+=g+d}return a}function d(a){try{return""!==a?JSON.parse(a):null}catch(b){throw Error(a);}}function g(a){return a.responseText}var k=0,m;return{xhr:function(e){var n=C.stream();void 0!==e.initialValue&&n(e.initialValue);var v="boolean"===typeof e.useBody?e.useBody:"GET"!==e.method&&"TRACE"!==e.method; -"function"!==typeof e.serialize&&(e.serialize="undefined"!==typeof FormData&&e.data instanceof FormData?function(a){return a}:JSON.stringify);"function"!==typeof e.deserialize&&(e.deserialize=d);"function"!==typeof e.extract&&(e.extract=g);e.url=b(e.url,e.data);v?e.data=e.serialize(e.data):e.url=h(e.url,e.data);var k=new a.XMLHttpRequest;k.open(e.method,e.url,"boolean"===typeof e.async?e.async:!0,"string"===typeof e.user?e.user:void 0,"string"===typeof e.password?e.password:void 0);e.serialize=== -JSON.stringify&&v&&k.setRequestHeader("Content-Type","application/json; charset=utf-8");e.deserialize===d&&k.setRequestHeader("Accept","application/json, text/*");"function"===typeof e.config&&(k=e.config(k,e)||k);k.onreadystatechange=function(){if(4===k.readyState){try{var a=e.deserialize(e.extract(k,e));if(200<=k.status&&300>k.status){if("function"===typeof e.type)if(a instanceof Array)for(var b=0;ba.indexOf("?")?"?":"&";a+=g+d}return a}function d(a){try{return""!==a?JSON.parse(a):null}catch(b){throw Error(a);}}function g(a){return a.responseText}var k=0,m;return{xhr:function(e){var n=C.stream();void 0!==e.initialValue&&n(e.initialValue);var v="boolean"===typeof e.useBody?e.useBody:"GET"!==e.method&& +"TRACE"!==e.method;"function"!==typeof e.serialize&&(e.serialize="undefined"!==typeof FormData&&e.data instanceof FormData?function(a){return a}:JSON.stringify);"function"!==typeof e.deserialize&&(e.deserialize=d);"function"!==typeof e.extract&&(e.extract=g);e.url=b(e.url,e.data);v?e.data=e.serialize(e.data):e.url=h(e.url,e.data);var k=new a.XMLHttpRequest;k.open(e.method,e.url,"boolean"===typeof e.async?e.async:!0,"string"===typeof e.user?e.user:void 0,"string"===typeof e.password?e.password:void 0); +e.serialize===JSON.stringify&&v&&k.setRequestHeader("Content-Type","application/json; charset=utf-8");e.deserialize===d&&k.setRequestHeader("Accept","application/json, text/*");"function"===typeof e.config&&(k=e.config(k,e)||k);k.onreadystatechange=function(){if(4===k.readyState){try{var a=e.deserialize(e.extract(k,e));if(200<=k.status&&300>k.status){if("function"===typeof e.type)if(a instanceof Array)for(var b=0;b