diff --git a/api/router.js b/api/router.js index 92731e77..75d2101e 100644 --- a/api/router.js +++ b/api/router.js @@ -2,38 +2,56 @@ var Vnode = require("../render/vnode") var coreRouter = require("../router/router") -var autoredraw = require("../api/autoredraw") -module.exports = function($window, renderer, pubsub) { +module.exports = function($window, mount) { var router = coreRouter($window) + var currentResolve, currentComponent, currentRender, currentArgs, currentPath + + var RouteComponent = {view: function() { + return currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined)) + }} + function defaultRender(vnode) { + return vnode + } var route = function(root, defaultRoute, routes) { - var current = {path: null, component: "div", resolver: null}, currentResolutionIdentifier = null - var replay = router.defineRoutes(routes, function(payload, args, path, route) { - var resolutionIdentifier = currentResolutionIdentifier = {} - function resolve(component) { - if (resolutionIdentifier !== currentResolutionIdentifier) return - resolutionIdentifier = null - current.path = path, current.component = component - renderer.render(root, payload.render(Vnode(component, null, args, undefined, undefined, undefined))) + currentComponent = "div" + currentRender = defaultRender + currentArgs = null + + mount(root, RouteComponent) + + router.defineRoutes(routes, function(payload, args, path) { + var isResolver = typeof payload.view !== "function" + var render = defaultRender + + var resolve = currentResolve = function (component) { + if (resolve !== currentResolve) return + currentResolve = null + + currentComponent = component != null ? component : isResolver ? "div" : payload + currentRender = render + currentArgs = args + currentPath = path + + root.redraw(true) } - if (typeof payload.view !== "function") { - if (typeof payload.render !== "function") payload.render = function(vnode) {return vnode} - if (typeof payload.onmatch !== "function") payload.onmatch = function() {resolve(current.component)} - if (path !== current.path) payload.onmatch(Vnode(payload, null, args, undefined, undefined, undefined), resolve) - else resolve(current.component) + var onmatch = function() { + resolve() } - else { - renderer.render(root, Vnode(payload, null, args, undefined, undefined, undefined)) + if (isResolver) { + if (typeof payload.render === "function") render = payload.render.bind(payload) + if (typeof payload.onmatch === "function") onmatch = payload.onmatch } + + onmatch.call(payload, resolve, args, path) }, function() { router.setPath(defaultRoute, null, {replace: true}) }) - autoredraw(root, renderer, pubsub, replay) } route.link = router.link route.prefix = router.setPrefix route.set = router.setPath - route.get = router.getPath - + route.get = function() {return currentPath} + return route } diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index ee383c65..b65daf4a 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -22,6 +22,16 @@ o.spec("mount", function() { render = coreRenderer($window).render }) + o("throws on invalid `root` DOM node", function() { + var threw = false + try { + mount(null, {view: function() {}}) + } catch (e) { + threw = true + } + o(threw).equals(true) + }) + o("renders into `root`", function() { mount(root, { view : function() { diff --git a/api/tests/test-router.js b/api/tests/test-router.js index d62c28b9..d43f0b44 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -8,13 +8,14 @@ var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") var apiPubSub = require("../../api/pubsub") var apiRouter = require("../../api/router") +var apiMounter = require("../../api/mount") o.spec("route", function() { 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 + var $window, root, redraw, mount, route o.beforeEach(function() { $window = browserMock(env) @@ -22,11 +23,22 @@ o.spec("route", function() { root = $window.document.body redraw = apiPubSub() - route = apiRouter($window, coreRenderer($window), redraw) + mount = apiMounter(coreRenderer($window), redraw) + route = apiRouter($window, mount) route.prefix(prefix) }) - o("renders into `root`", function(done) { + o("throws on invalid `root` DOM node", function() { + var threw = false + try { + route(null, '/', {'/':{view: function() {}}}) + } catch (e) { + threw = true + } + o(threw).equals(true) + }) + + o("renders into `root`", function() { $window.location.href = prefix + "/" route(root, "/", { "/" : { @@ -36,11 +48,21 @@ o.spec("route", function() { } }) - callAsync(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }) + o(root.firstChild.nodeName).equals("DIV") + }) + + o("routed mount points can redraw synchronoulsy (#1275)", function() { + var view = o.spy() + + $window.location.href = prefix + "/" + route(root, "/", {"/":{view:view}}) + + o(view.callCount).equals(1) + + redraw.publish(true) + + o(view.callCount).equals(2) + }) o("default route doesn't break back button", function(done) { @@ -55,11 +77,11 @@ o.spec("route", function() { setTimeout(function() { o(root.firstChild.nodeName).equals("DIV") - + $window.history.back() - + o($window.location.pathname).equals("/") - + done() }, FRAME_BUDGET) }) @@ -77,12 +99,12 @@ o.spec("route", function() { function init(vnode) { o(vnode.attrs.foo).equals(undefined) - + done() } }) - o("redraws when render function is executed", function(done) { + o("redraws when render function is executed", function() { var onupdate = o.spy() var oninit = o.spy() @@ -98,18 +120,11 @@ o.spec("route", function() { } }) - callAsync(function() { - o(oninit.callCount).equals(1) + o(oninit.callCount).equals(1) - redraw.publish() + redraw.publish(true) - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - done() - }, FRAME_BUDGET) - }) + o(onupdate.callCount).equals(1) }) o("redraws on events", function(done) { @@ -133,23 +148,21 @@ o.spec("route", function() { } }) - callAsync(function() { - root.firstChild.dispatchEvent(e) + root.firstChild.dispatchEvent(e) - o(oninit.callCount).equals(1) + 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) + 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) + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) - done() - }, FRAME_BUDGET) - }) + done() + }, FRAME_BUDGET * 2) }) o("event handlers can skip redraw", function(done) { @@ -175,21 +188,19 @@ o.spec("route", function() { } }) - callAsync(function() { - root.firstChild.dispatchEvent(e) + root.firstChild.dispatchEvent(e) - o(oninit.callCount).equals(1) + o(oninit.callCount).equals(1) - // Wrapped to ensure no redraw fired - setTimeout(function() { - o(onupdate.callCount).equals(0) + // Wrapped to ensure no redraw fired + setTimeout(function() { + o(onupdate.callCount).equals(0) - done() - }, FRAME_BUDGET) - }) + done() + }, FRAME_BUDGET) }) - o("changes location on route.link", function(done) { + o("changes location on route.link", function() { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -211,20 +222,16 @@ o.spec("route", function() { } }) - callAsync(function() { - var slash = prefix[0] === "/" ? "" : "/" + var slash = prefix[0] === "/" ? "" : "/" - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "")) - root.firstChild.dispatchEvent(e) + root.firstChild.dispatchEvent(e) - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - - done() - }) + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") }) - - o("accepts RouteResolver", function(done) { + + o("accepts RouteResolver", function() { var matchCount = 0 var renderCount = 0 var Component = { @@ -232,107 +239,93 @@ o.spec("route", function() { return m("div") } } - - $window.location.href = prefix + "/" + + $window.location.href = prefix + "/abc" route(root, "/abc", { "/:id" : { - onmatch: function(vnode, resolve) { + onmatch: function(resolve, args, requestedPath) { matchCount++ - - o(vnode.attrs.id).equals("abc") - o(route.get()).equals("/abc") - + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + resolve(Component) }, render: function(vnode) { renderCount++ - + o(vnode.attrs.id).equals("abc") - + return vnode }, }, }) - - setTimeout(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) + + o(matchCount).equals(1) + o(renderCount).equals(1) + o(root.firstChild.nodeName).equals("DIV") }) - - o("accepts RouteResolver without `render` method as payload", function(done) { + + o("accepts RouteResolver without `render` method as payload", function() { var matchCount = 0 var Component = { view: function() { return m("div") } } - - $window.location.href = prefix + "/" + + $window.location.href = prefix + "/abc" route(root, "/abc", { "/:id" : { - onmatch: function(vnode, resolve) { + onmatch: function(resolve, args, requestedPath) { matchCount++ - - o(vnode.attrs.id).equals("abc") - o(route.get()).equals("/abc") - + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + resolve(Component) }, }, }) - - setTimeout(function() { - o(matchCount).equals(1) - - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) + + o(matchCount).equals(1) + + o(root.firstChild.nodeName).equals("DIV") }) - - o("accepts RouteResolver without `onmatch` method as payload", function(done) { + + o("accepts RouteResolver without `onmatch` method as payload", function() { var renderCount = 0 var Component = { view: function() { return m("div") } } - - $window.location.href = prefix + "/" + + $window.location.href = prefix + "/abc" route(root, "/abc", { "/:id" : { render: function(vnode) { renderCount++ - + o(vnode.attrs.id).equals("abc") - + return m(Component) }, }, }) - - setTimeout(function() { - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) + + o(root.firstChild.nodeName).equals("DIV") }) - - o("RouteResolver `render` does not have component semantics", function(done, timeout) { - timeout(60) - + + o("RouteResolver `render` does not have component semantics", function(done) { var renderCount = 0 var A = { view: function() { return m("div") } } - - $window.location.href = prefix + "/" + + $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { render: function(vnode) { @@ -345,22 +338,20 @@ o.spec("route", function() { }, }, }) - + + var dom = root.firstChild + o(root.firstChild.nodeName).equals("DIV") + + route.set("/b") + setTimeout(function() { - var dom = root.firstChild - o(root.firstChild.nodeName).equals("DIV") - - route.set("/b") - - setTimeout(function() { - o(root.firstChild).equals(dom) - - done() - }, FRAME_BUDGET) + o(root.firstChild).equals(dom) + + done() }, FRAME_BUDGET) }) - o("calls onmatch and view correct number of times", function(done) { + o("calls onmatch and view correct number of times", function() { var matchCount = 0 var renderCount = 0 var Component = { @@ -368,11 +359,11 @@ o.spec("route", function() { return m("div") } } - + $window.location.href = prefix + "/" route(root, "/", { "/" : { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { matchCount++ resolve(Component) }, @@ -383,69 +374,63 @@ o.spec("route", function() { }, }) - callAsync(function() { - o(matchCount).equals(1) - o(renderCount).equals(1) - - redraw.publish() + o(matchCount).equals(1) + o(renderCount).equals(1) - setTimeout(function() { - o(matchCount).equals(1) - o(renderCount).equals(2) - - done() - }, FRAME_BUDGET) - }) + redraw.publish(true) + + o(matchCount).equals(1) + o(renderCount).equals(2) }) - + o("onmatch can redirect to another route", function(done) { - var redirected = false + var redirected = false - $window.location.href = prefix + "/" - route(root, "/a", { - "/a" : { - onmatch: function() { - route.set("/b") - } - }, - "/b" : { - view: function(vnode){ - redirected = true - } - } - }) + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + route.set("/b") + } + }, + "/b" : { + view: function(vnode){ + redirected = true + } + } + }) - setTimeout(function() { - o(redirected).equals(true) + setTimeout(function() { + o(redirected).equals(true) + + done() + }, FRAME_BUDGET) + }) - done() - }, FRAME_BUDGET) - }) - o("onmatch can redirect to another route that has RouteResolver", function(done) { - var redirected = false + var redirected = false - $window.location.href = prefix + "/" - route(root, "/a", { - "/a" : { - onmatch: function() { - route.set("/b") - } - }, - "/b" : { - render: function(vnode){ - redirected = true - } - } - }) + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a" : { + onmatch: function() { + route.set("/b") + } + }, + "/b" : { + render: function(vnode){ + redirected = true + } + } + }) - setTimeout(function() { - o(redirected).equals(true) + setTimeout(function() { + o(redirected).equals(true) + + done() + }, FRAME_BUDGET) + }) - done() - }, FRAME_BUDGET) - }) - o("onmatch resolution callback resolves at most once", function(done) { var resolveCount = 0 var resolvedComponent @@ -456,7 +441,7 @@ o.spec("route", function() { $window.location.href = prefix + "/" route(root, "/", { "/": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { resolve(A) resolve(B) callAsync(function() {resolve(C)}) @@ -474,15 +459,114 @@ o.spec("route", function() { done() }, FRAME_BUDGET) }) - - o("calling route.set invalidates pending onmatch resolution", function(done, timeout) { - timeout(100) - - var resolved + + o("the previous view redraws while onmatch resolution is pending (#1268)", function(done) { + var view = o.spy() + var onmatch = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/", { + "/a": {view: view}, + "/b": {onmatch: onmatch} + }) + + o(view.callCount).equals(1) + o(onmatch.callCount).equals(0) + + route.set("/b") + + setTimeout(function(){ + o(view.callCount).equals(1) + o(onmatch.callCount).equals(1) + + redraw.publish(true) + + o(view.callCount).equals(2) + o(onmatch.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + + o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ + var onmatch = o.spy(function(resolve){resolve()}) + $window.location.href = prefix + "/" + route(root, '/', { + "/":{ + onmatch: onmatch, + render: function(){return m("div")} + } + }) + + o(onmatch.callCount).equals(1) + + route.set(route.get()) + + setTimeout(function() { + o(onmatch.callCount).equals(2) + + done() + }, FRAME_BUDGET) + }) + + o("m.route.get() returns the last fully resolved route (#1276)", function(done){ + $window.location.href = prefix + "/" + + route(root, "/", { + "/": {view: function(){}}, + "/2": {onmatch: function(){}} + }) + + + o(route.get()).equals("/") + + route.set("/2") + + setTimeout(function(){ + o(route.get()).equals("/") + done() + }, FRAME_BUDGET) + }) + + o("routing with RouteResolver works more than once (#1286)", function(done, timeout){ + timeout(FRAME_BUDGET * 3) + + $window.location.href = prefix + "/a" + route(root, '/a', { + '/a': { + render: function() { + return m("a", "a") + } + }, + '/b': { + render: function() { + return m("b", "b") + } + } + }) + + route.set('/b') + + setTimeout(function(){ + route.set('/a') + + setTimeout(function(){ + o(root.firstChild.nodeName).equals("A") + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + }) + + o("calling route.set invalidates pending onmatch resolution", function(done, timeout) { + timeout(50) + + var resolved + $window.location.href = prefix + "/a" route(root, "/a", { "/a": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { setTimeout(resolve, 20) }, render: function(vnode) {resolved = "a"} @@ -491,15 +575,14 @@ o.spec("route", function() { view: function() {resolved = "b"} } }) - setTimeout(function() { - route.set("/b") - - setTimeout(function() { - o(resolved).equals("b") - done() - }, 30) - }, FRAME_BUDGET) + route.set("/b") + + setTimeout(function() { + o(resolved).equals("b") + + done() + }, 30) }) }) }) diff --git a/docs/route.md b/docs/route.md index c4175e84..30d31a11 100644 --- a/docs/route.md +++ b/docs/route.md @@ -2,13 +2,13 @@ - [API](#api) - [Static members](#static-members) - - [route.set](#route-set) - - [route.get](#route-get) - - [route.prefix](#route-prefix) - - [route.link](#route-link) + - [route.set](#routeset) + - [route.get](#routeget) + - [route.prefix](#routeprefix) + - [route.link](#routelink) - [RouteResolver](#routeresolver) - - [routeResolver.onmatch](#routeresolver-onmatch) - - [routeResolver.render](#routeresolver-render) + - [routeResolver.onmatch](#routeresolveronmatch) + - [routeResolver.render](#routeresolverrender) - [How it works](#how-it-works) - [Typical usage](#typical-usage) - [Navigating to different routes](#navigating-to-different-routes) @@ -52,13 +52,13 @@ Argument | Type | Required | Description ##### route.get -Returns the current routing path, without the prefix. +Returns the last fully resolved routing path, without the prefix. It may differ from the path displayed in the location bar while an asynchronous route is [pending resolution](#code-splitting). `path = m.route.get()` Argument | Type | Required | Description ----------------- | --------- | -------- | --- -**returns** | String | | Returns the current path +**returns** | String | | Returns the last fully resolved path ##### route.prefix @@ -94,14 +94,12 @@ This method also allows you to asynchronously define what component will be rend `routeResolver.onmatch(vnode, resolve)` -Argument | Type | Description -------------------- | --------------------- | --- -`vnode` | `Vnode` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If the routeResolver does not have a `resolve` method, the vnode's `tag` field defaults to a `div` -`vnode.attrs` | `Object` | The [routing parameters](#routing-parameters) -`vnode.attrs.path` | `String` | The current router path, including interpolated routing parameter values, but without the prefix. Same value as `m.route.get()` -`vnode.attrs.route` | `String` | The matched route -`resolve` | `Function(Component)` | Call this function with a component as the first argument to use it as the route's component -**returns** | | Returns `undefined` +Argument | Type | Description +--------------- | --------------------- | --- +`resolve` | `Function(Component)` | Call this function with a component as the first argument to use it as the route's component +`args` | `Object` | The [routing parameters](#routing-parameters) +`requestedPath` | `String` | The router path requested by the last routing action, including interpolated routing parameter values, but without the prefix. When `onmatch` is called, the resolution for this path is not complete and `m.route.get()` still returns the previous path. +**returns** | | Returns `undefined` ##### routeResolver.render @@ -113,8 +111,6 @@ Argument | Type | Description ------------------- | --------------- | ----------- `vnode` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If the routeResolver does not have a `resolve` method, the vnode's `tag` field defaults to a `div` `vnode.attrs` | `Object` | A [vnode](vnodes.md) whose attributes object contains routing parameters. If the routeResolver does not have a `resolve` method, the vnode defaults to a `div` -`vnode.attrs.path` | `String` | The current router path, including interpolated routing parameter values, but without the prefix. Same value as `m.route.get()` -`vnode.attrs.route` | `String` | The matched route **returns** | `Vnode` | Returns a vnode --- @@ -271,7 +267,7 @@ Instead of mapping a component to a route, you can specify a RouteResolver objec ```javascript m.route(document.body, "/", { "/": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve, args, requestedPath) { resolve(Home) }, render: function(vnode) { @@ -351,7 +347,7 @@ var Login = { m.route(document.body, "/secret", { "/secret": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { if (isLoggedIn) resolve(Home) else m.route.set("/login") }, @@ -399,7 +395,7 @@ function load(file, done) { m.route(document.body, "/", { "/": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { load("Home.js", resolve) }, }, @@ -413,7 +409,7 @@ Fortunately, there are a number of tools that facilitate the task of bundling mo ```javascript m.route(document.body, "/", { "/": { - onmatch: function(vnode, resolve) { + onmatch: function(resolve) { // using Webpack async code splitting require(['./Home.js'], resolve) }, diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index 11a6b2c9..f2e03967 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -209,7 +209,7 @@ m.route(element, '/', { ## `m.route` mode -`m.route.mode` was replaced by `m.route.prefix(prefix)` where `prefix` can be `#`, `?`, `` (for "pathname" mode). The new API also supports hashbang (`#!`), which is the default, and it supports non-root pathnames and arbitrary mode variations such as querybang (`?!`) +`m.route.mode` was replaced by `m.route.prefix(prefix)` where `prefix` can be `#`, `?`, or an empty string (for "pathname" mode). The new API also supports hashbang (`#!`), which is the default, and it supports non-root pathnames and arbitrary mode variations such as querybang (`?!`) ## `m.route()` and anchor tags diff --git a/index.js b/index.js index 6155ba71..6eb607f3 100644 --- a/index.js +++ b/index.js @@ -10,8 +10,8 @@ var Stream = require("./stream") requestService.setCompletionCallback(redrawService.publish) -m.route = require("./route") m.mount = require("./mount") +m.route = require("./route") m.withAttr = require("./util/withAttr") m.prop = Stream m.render = renderService.render diff --git a/mithril.js b/mithril.js index cc497e8e..5edc18af 100644 --- a/mithril.js +++ b/mithril.js @@ -573,6 +573,7 @@ var renderService = function($window) { Object.keys(source).forEach(function(k){target[k] = source[k]}) } function render(dom, vnodes) { + if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = $doc.activeElement // First time rendering into a node clears it out @@ -967,6 +968,56 @@ var parseQueryString = function(string) { return data } requestService.setCompletionCallback(redrawService.publish) +var throttle = function(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function(synchronous) { + var now = Date.now() + if (synchronous === true || last === 0 || now - last >= time) { + last = now + callback() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, time - (now - last)) + } + } +} +var autoredraw = function(root, renderer, pubsub, callback) { + var run = throttle(callback) + if (renderer != null) { + renderer.setEventCallback(function(e) { + if (e.redraw !== false) pubsub.publish() + }) + } + if (pubsub != null) { + if (root.redraw) pubsub.unsubscribe(root.redraw) + pubsub.subscribe(run) + } + return root.redraw = run +} +m.mount = function(renderer, pubsub) { + return function(root, component) { + if (component === null) { + renderer.render(root, []) + pubsub.unsubscribe(root.redraw) + delete root.redraw + return + } + var run = autoredraw(root, renderer, pubsub, function() { + renderer.render( + root, + Vnode(component, undefined, undefined, undefined, undefined, undefined) + ) + }) + run() + } +}(renderService, redrawService) var coreRouter = function($window) { var supportsPushState = typeof $window.history.pushState === "function" var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout @@ -977,6 +1028,16 @@ var coreRouter = function($window) { if (fragment === "pathname" && data[0] !== "/") data = "/" + data return data } + var asyncId + function debounceAsync(f) { + return function() { + if (asyncId != null) return + asyncId = callAsync(function() { + asyncId = null + f() + }) + } + } function parsePath(path, queryData, hashData) { var queryIndex = path.indexOf("?") var hashIndex = path.indexOf("#") @@ -1022,7 +1083,7 @@ var coreRouter = function($window) { else $window.location.href = prefix + path } function defineRoutes(routes, resolve, reject) { - if (supportsPushState) $window.onpopstate = resolveRoute + if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) else if (prefix.charAt(0) === "#") $window.onhashchange = resolveRoute resolveRoute() @@ -1031,23 +1092,21 @@ var coreRouter = function($window) { 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, "([^\\/]+)") + "\/?$") - if (matcher.test(pathname)) { - pathname.replace(matcher, function() { - var keys = route.match(/:[^\/]+/g) || [] - var values = [].slice.call(arguments, 1, -2) - for (var i = 0; i < keys.length; i++) { - params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) - } - resolve(routes[route], params, path, route) - }) - return - } + for (var route in routes) { + var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") + if (matcher.test(pathname)) { + pathname.replace(matcher, function() { + var keys = route.match(/:[^\/]+/g) || [] + var values = [].slice.call(arguments, 1, -2) + for (var i = 0; i < keys.length; i++) { + params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) + } + resolve(routes[route], params, path, route) + }) + return } - reject(path, params) - }) + } + reject(path, params) } return resolveRoute } @@ -1061,89 +1120,52 @@ var coreRouter = function($window) { } return {setPrefix: setPrefix, getPath: getPath, setPath: setPath, defineRoutes: defineRoutes, link: link} } -var throttle = function(callback) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function(synchronous) { - var now = Date.now() - if (synchronous === true || last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback() - last = Date.now() - }, time - (now - last)) - } - } -} -var autoredraw = function(root, renderer, pubsub, callback) { - var run = throttle(callback) - if (renderer != null) { - renderer.setEventCallback(function(e) { - if (e.redraw !== false) pubsub.publish() - }) - } - if (pubsub != null) { - if (root.redraw) pubsub.unsubscribe(root.redraw) - pubsub.subscribe(run) - } - return root.redraw = run -} -m.route = function($window, renderer, pubsub) { +m.route = function($window, mount1) { var router = coreRouter($window) + var globalId, currentComponent, currentRender, currentArgs, currentPath + var RouteComponent = {view: function() { + return currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined)) + }} + function defaultRender(vnode) { + return vnode + } var route = function(root, defaultRoute, routes) { - var current = {path: null, component: "div", resolver: null}, currentResolutionIdentifier = null - var replay = router.defineRoutes(routes, function(payload, args, path, route) { - var resolutionIdentifier = currentResolutionIdentifier = {} - function resolve(component) { - if (resolutionIdentifier !== currentResolutionIdentifier) return - resolutionIdentifier = null - current.path = path, current.component = component - renderer.render(root, payload.render(Vnode(component, null, args, undefined, undefined, undefined))) + currentComponent = "div" + currentRender = defaultRender + currentArgs = null + mount1(root, RouteComponent) + router.defineRoutes(routes, function(payload, args, path) { + var resolutionIdentifier = globalId = {} + var isResolver = typeof payload.view !== "function" + var render = defaultRender + function resolve (component) { + if (resolutionIdentifier !== globalId) return + globalId = null + currentComponent = component != null ? component : isResolver ? "div" : payload + currentRender = render + currentArgs = args + currentPath = path + root.redraw(true) } - if (typeof payload.view !== "function") { - if (typeof payload.render !== "function") payload.render = function(vnode) {return vnode} - if (typeof payload.onmatch !== "function") payload.onmatch = function() {resolve(current.component)} - if (path !== current.path) payload.onmatch(Vnode(payload, null, args, undefined, undefined, undefined), resolve) - else resolve(current.component) + var onmatch = function() { + resolve() } - else { - renderer.render(root, Vnode(payload, null, args, undefined, undefined, undefined)) + if (isResolver) { + if (typeof payload.render === "function") render = payload.render.bind(payload) + if (typeof payload.onmatch === "function") onmatch = payload.onmatch } + + onmatch.call(payload, resolve, args, path) }, function() { router.setPath(defaultRoute, null, {replace: true}) }) - autoredraw(root, renderer, pubsub, replay) } route.link = router.link route.prefix = router.setPrefix route.set = router.setPath - route.get = router.getPath - + route.get = function() {return currentPath} return route -}(window, renderService, redrawService) -m.mount = function(renderer, pubsub) { - return function(root, component) { - if (component === null) { - renderer.render(root, []) - pubsub.unsubscribe(root.redraw) - delete root.redraw - return - } - var run = autoredraw(root, renderer, pubsub, function() { - renderer.render( - root, - Vnode(component, undefined, undefined, undefined, undefined, undefined) - ) - }) - run() - } -}(renderService, redrawService) +}(window, m.mount) m.withAttr = function(attrName, callback, context) { return function(e) { return callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) diff --git a/mithril.min.js b/mithril.min.js index a8f12318..a45a8e48 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,42 @@ -new function(){function u(d,e,p,g,k,m){return{tag:d,key:e,attrs:p,children:g,text:k,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function x(d){if(null==d||"string"!==typeof d&&null==d.view)throw Error("The selector must be either a string or a component.");if("string"===typeof d&&void 0===H[d]){for(var e,p,g=[],k={};e=P.exec(d);){var m=e[1],n=e[2];""===m&&""!==n?p=n:"#"===m?k.id=n:"."===m?g.push(n):"["===e[3][0]&&((m=e[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -k[e[4]]=m||!0)}0=r&&z>=B;){var v=a[r],w=f[B];if(v===w)r++,B++;else if(null!=v&&null!=w&&v.key===w.key)r++,B++,m(b,v,w,d,l(a,r,h),q,g),q&&v.tag===w.tag&&t(b,n(v),h);else if(v=a[k],v===w)k--,B++;else if(null!=v&&null!=w&&v.key===w.key)m(b,v,w,d,l(a,k+1,h),q,g),B=r&&z>=B;){v=a[k];w=f[z];if(v===w)k--;else if(null!=v&&null!=w&&v.key===w.key)m(b,v,w,d,l(a,k+1,h),q,g),q&&v.tag===w.tag&&t(b,n(v),h),null!=v.dom&&(h=v.dom),k--;else{if(!A){A=a;var v=k,u={},y;for(y=0;yc.indexOf("?")?"?":"&";c+=h+e}return c}function k(c){try{return""!==c?JSON.parse(c):null}catch(d){throw Error(c); -}}function m(c){return c.responseText}function n(c,d){if("function"===typeof c)if(d instanceof Array)for(var e=0;eh.status)l(n(c.type,d));else{var e=Error(h.responseText),g;for(g in d)e[g]=d[g];l.error(e)}}catch(k){l.error(k)}"function"===typeof t&&t()}};A?h.send(c.data):h.send();return l},jsonp:function(c){var k=e();void 0!==c.initialValue&&k(c.initialValue);var m=c.callbackName||"_mithril_"+Math.round(1E16*Math.random())+ -"_"+l++,h=d.document.createElement("script");d[m]=function(e){h.parentNode.removeChild(h);k(n(c.type,e));"function"===typeof t&&t();delete d[m]};h.onerror=function(){h.parentNode.removeChild(h);k.error(Error("JSONP request failed"));"function"===typeof t&&t();delete d[m]};null==c.data&&(c.data={});c.url=p(c.url,c.data);c.data[c.callbackKey||"callback"]=m;h.src=g(c.url,c.data);d.document.documentElement.appendChild(h);return k},setCompletionCallback:function(c){t=c}}}(window,N),G=function(){var d= -[];return{subscribe:d.push.bind(d),unsubscribe:function(e){e=d.indexOf(e);-1=v&&A>=E;){var r=a[v],u=e[E];if(r===u)v++,E++;else if(null!=r&&null!=u&&r.key===u.key)v++,E++,l(c,r,u,b,w(a,v,g),p,h),p&&r.tag===u.tag&&q(c,m(r),g);else if(r=a[k],r===u)k--,E++;else if(null!=r&&null!=u&&r.key===u.key)l(c,r,u,b,w(a,k+1,g),p,h),E=v&&A>=E;){r=a[k];u=e[A];if(r===u)k--;else if(null!=r&&null!=u&&r.key===u.key)l(c,r,u,b,w(a,k+1,g),p,h),p&&r.tag===u.tag&&q(c,m(r),g),null!=r.dom&&(g=r.dom),k--;else{if(!t){t=a;var r=k,C={},z;for(z=0;zb.indexOf("?")?"?":"&";b+=h+f}return b}function k(b){try{return""!==b?JSON.parse(b):null}catch(g){throw Error(b); +}}function l(b){return b.responseText}function m(b,g){if("function"===typeof b)if(g instanceof Array)for(var f=0;ft.status)g(m(d.type,b));else{var f=Error(t.responseText),h;for(h in b)f[h]=b[h];g.error(f)}}catch(k){g.error(k)}"function"===typeof q&&q()}};A?t.send(d.data):t.send();return g},jsonp:function(d){var g=f();void 0!==d.initialValue&&g(d.initialValue);var k=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+ +"_"+w++,t=b.document.createElement("script");b[k]=function(f){t.parentNode.removeChild(t);g(m(d.type,f));"function"===typeof q&&q();delete b[k]};t.onerror=function(){t.parentNode.removeChild(t);g.error(Error("JSONP request failed"));"function"===typeof q&&q();delete b[k]};null==d.data&&(d.data={});d.url=n(d.url,d.data);d.data[d.callbackKey||"callback"]=k;t.src=h(d.url,d.data);b.document.documentElement.appendChild(t);return g},setCompletionCallback:function(b){q=b}}}(window,O),L=function(){var b= +[];return{subscribe:b.push.bind(b),unsubscribe:function(f){f=b.indexOf(f);-1