Merge pull request #1291 from pygy/debounceAsync

Router: Add debounceAsync to normalize the timing of onhashchange and onpopstate.
This commit is contained in:
Leo Horie 2016-09-07 09:51:15 -04:00 committed by GitHub
commit 6a7b048064
8 changed files with 353 additions and 258 deletions

View file

@ -2,38 +2,57 @@
var Vnode = require("../render/vnode") var Vnode = require("../render/vnode")
var coreRouter = require("../router/router") 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 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 route = function(root, defaultRoute, routes) {
var current = {path: null, component: "div", resolver: null}, currentResolutionIdentifier = null currentComponent = "div"
var replay = router.defineRoutes(routes, function(payload, args, path, route) { currentRender = defaultRender
var resolutionIdentifier = currentResolutionIdentifier = {} currentArgs = null
function resolve(component) {
if (resolutionIdentifier !== currentResolutionIdentifier) return mount(root, RouteComponent)
resolutionIdentifier = null
current.path = path, current.component = component router.defineRoutes(routes, function(payload, args, path) {
renderer.render(root, payload.render(Vnode(component, null, args, undefined, undefined, undefined))) 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") { var onmatch = function() {
if (typeof payload.render !== "function") payload.render = function(vnode) {return vnode} resolve()
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)
} }
else { if (isResolver) {
renderer.render(root, Vnode(payload, null, args, undefined, undefined, undefined)) 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() { }, function() {
router.setPath(defaultRoute, null, {replace: true}) router.setPath(defaultRoute, null, {replace: true})
}) })
autoredraw(root, renderer, pubsub, replay)
} }
route.link = router.link route.link = router.link
route.prefix = router.setPrefix route.prefix = router.setPrefix
route.set = router.setPath route.set = router.setPath
route.get = router.getPath route.get = function() {return currentPath}
return route return route
} }

View file

@ -8,13 +8,14 @@ var m = require("../../render/hyperscript")
var coreRenderer = require("../../render/render") var coreRenderer = require("../../render/render")
var apiPubSub = require("../../api/pubsub") var apiPubSub = require("../../api/pubsub")
var apiRouter = require("../../api/router") var apiRouter = require("../../api/router")
var apiMounter = require("../../api/mount")
o.spec("route", function() { o.spec("route", function() {
void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) {
void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) {
o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() {
var FRAME_BUDGET = Math.floor(1000 / 60) var FRAME_BUDGET = Math.floor(1000 / 60)
var $window, root, redraw, route var $window, root, redraw, mount, route
o.beforeEach(function() { o.beforeEach(function() {
$window = browserMock(env) $window = browserMock(env)
@ -22,11 +23,12 @@ o.spec("route", function() {
root = $window.document.body root = $window.document.body
redraw = apiPubSub() redraw = apiPubSub()
route = apiRouter($window, coreRenderer($window), redraw) mount = apiMounter(coreRenderer($window), redraw)
route = apiRouter($window, mount)
route.prefix(prefix) route.prefix(prefix)
}) })
o("renders into `root`", function(done) { o("renders into `root`", function() {
$window.location.href = prefix + "/" $window.location.href = prefix + "/"
route(root, "/", { route(root, "/", {
"/" : { "/" : {
@ -36,11 +38,21 @@ o.spec("route", function() {
} }
}) })
callAsync(function() { o(root.firstChild.nodeName).equals("DIV")
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)
done()
})
}) })
o("default route doesn't break back button", function(done) { o("default route doesn't break back button", function(done) {
@ -82,7 +94,7 @@ o.spec("route", function() {
} }
}) })
o("redraws when render function is executed", function(done) { o("redraws when render function is executed", function() {
var onupdate = o.spy() var onupdate = o.spy()
var oninit = o.spy() var oninit = o.spy()
@ -98,18 +110,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 o(onupdate.callCount).equals(1)
setTimeout(function() {
o(onupdate.callCount).equals(1)
done()
}, FRAME_BUDGET)
})
}) })
o("redraws on events", function(done) { o("redraws on events", function(done) {
@ -133,23 +138,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.callCount).equals(1)
o(onclick.this).equals(root.firstChild) o(onclick.this).equals(root.firstChild)
o(onclick.args[0].type).equals("click") o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild) o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire // Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() { setTimeout(function() {
o(onupdate.callCount).equals(1) o(onupdate.callCount).equals(1)
done() done()
}, FRAME_BUDGET) }, FRAME_BUDGET * 2)
})
}) })
o("event handlers can skip redraw", function(done) { o("event handlers can skip redraw", function(done) {
@ -175,21 +178,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 // Wrapped to ensure no redraw fired
setTimeout(function() { setTimeout(function() {
o(onupdate.callCount).equals(0) o(onupdate.callCount).equals(0)
done() done()
}, FRAME_BUDGET) }, FRAME_BUDGET)
})
}) })
o("changes location on route.link", function(done) { o("changes location on route.link", function() {
var e = $window.document.createEvent("MouseEvents") var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true) e.initEvent("click", true, true)
@ -211,20 +212,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") o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test")
done()
})
}) })
o("accepts RouteResolver", function(done) { o("accepts RouteResolver", function() {
var matchCount = 0 var matchCount = 0
var renderCount = 0 var renderCount = 0
var Component = { var Component = {
@ -233,14 +230,14 @@ o.spec("route", function() {
} }
} }
$window.location.href = prefix + "/" $window.location.href = prefix + "/abc"
route(root, "/abc", { route(root, "/abc", {
"/:id" : { "/:id" : {
onmatch: function(vnode, resolve) { onmatch: function(resolve, args, requestedPath) {
matchCount++ matchCount++
o(vnode.attrs.id).equals("abc") o(args.id).equals("abc")
o(route.get()).equals("/abc") o(requestedPath).equals("/abc")
resolve(Component) resolve(Component)
}, },
@ -254,16 +251,12 @@ o.spec("route", function() {
}, },
}) })
setTimeout(function() { o(matchCount).equals(1)
o(matchCount).equals(1) o(renderCount).equals(1)
o(renderCount).equals(1) o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.nodeName).equals("DIV")
done()
}, FRAME_BUDGET)
}) })
o("accepts RouteResolver without `render` method as payload", function(done) { o("accepts RouteResolver without `render` method as payload", function() {
var matchCount = 0 var matchCount = 0
var Component = { var Component = {
view: function() { view: function() {
@ -271,30 +264,26 @@ o.spec("route", function() {
} }
} }
$window.location.href = prefix + "/" $window.location.href = prefix + "/abc"
route(root, "/abc", { route(root, "/abc", {
"/:id" : { "/:id" : {
onmatch: function(vnode, resolve) { onmatch: function(resolve, args, requestedPath) {
matchCount++ matchCount++
o(vnode.attrs.id).equals("abc") o(args.id).equals("abc")
o(route.get()).equals("/abc") o(requestedPath).equals("/abc")
resolve(Component) resolve(Component)
}, },
}, },
}) })
setTimeout(function() { o(matchCount).equals(1)
o(matchCount).equals(1)
o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.nodeName).equals("DIV")
done()
}, FRAME_BUDGET)
}) })
o("accepts RouteResolver without `onmatch` method as payload", function(done) { o("accepts RouteResolver without `onmatch` method as payload", function() {
var renderCount = 0 var renderCount = 0
var Component = { var Component = {
view: function() { view: function() {
@ -302,7 +291,7 @@ o.spec("route", function() {
} }
} }
$window.location.href = prefix + "/" $window.location.href = prefix + "/abc"
route(root, "/abc", { route(root, "/abc", {
"/:id" : { "/:id" : {
render: function(vnode) { render: function(vnode) {
@ -315,16 +304,10 @@ o.spec("route", function() {
}, },
}) })
setTimeout(function() { o(root.firstChild.nodeName).equals("DIV")
o(root.firstChild.nodeName).equals("DIV")
done()
}, FRAME_BUDGET)
}) })
o("RouteResolver `render` does not have component semantics", function(done, timeout) { o("RouteResolver `render` does not have component semantics", function(done) {
timeout(60)
var renderCount = 0 var renderCount = 0
var A = { var A = {
view: function() { view: function() {
@ -332,7 +315,7 @@ o.spec("route", function() {
} }
} }
$window.location.href = prefix + "/" $window.location.href = prefix + "/a"
route(root, "/a", { route(root, "/a", {
"/a" : { "/a" : {
render: function(vnode) { render: function(vnode) {
@ -346,21 +329,19 @@ o.spec("route", function() {
}, },
}) })
var dom = root.firstChild
o(root.firstChild.nodeName).equals("DIV")
route.set("/b")
setTimeout(function() { setTimeout(function() {
var dom = root.firstChild o(root.firstChild).equals(dom)
o(root.firstChild.nodeName).equals("DIV")
route.set("/b") done()
setTimeout(function() {
o(root.firstChild).equals(dom)
done()
}, FRAME_BUDGET)
}, FRAME_BUDGET) }, 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 matchCount = 0
var renderCount = 0 var renderCount = 0
var Component = { var Component = {
@ -372,7 +353,7 @@ o.spec("route", function() {
$window.location.href = prefix + "/" $window.location.href = prefix + "/"
route(root, "/", { route(root, "/", {
"/" : { "/" : {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
matchCount++ matchCount++
resolve(Component) resolve(Component)
}, },
@ -383,68 +364,62 @@ o.spec("route", function() {
}, },
}) })
callAsync(function() { o(matchCount).equals(1)
o(matchCount).equals(1) o(renderCount).equals(1)
o(renderCount).equals(1)
redraw.publish() redraw.publish(true)
setTimeout(function() { o(matchCount).equals(1)
o(matchCount).equals(1) o(renderCount).equals(2)
o(renderCount).equals(2)
done()
}, FRAME_BUDGET)
})
}) })
o("onmatch can redirect to another route", function(done) { o("onmatch can redirect to another route", function(done) {
var redirected = false var redirected = false
$window.location.href = prefix + "/" $window.location.href = prefix + "/a"
route(root, "/a", { route(root, "/a", {
"/a" : { "/a" : {
onmatch: function() { onmatch: function() {
route.set("/b") route.set("/b")
} }
}, },
"/b" : { "/b" : {
view: function(vnode){ view: function(vnode){
redirected = true redirected = true
} }
} }
}) })
setTimeout(function() { setTimeout(function() {
o(redirected).equals(true) o(redirected).equals(true)
done() done()
}, FRAME_BUDGET) }, FRAME_BUDGET)
}) })
o("onmatch can redirect to another route that has RouteResolver", function(done) { o("onmatch can redirect to another route that has RouteResolver", function(done) {
var redirected = false var redirected = false
$window.location.href = prefix + "/" $window.location.href = prefix + "/a"
route(root, "/a", { route(root, "/a", {
"/a" : { "/a" : {
onmatch: function() { onmatch: function() {
route.set("/b") route.set("/b")
} }
}, },
"/b" : { "/b" : {
render: function(vnode){ render: function(vnode){
redirected = true redirected = true
} }
} }
}) })
setTimeout(function() { setTimeout(function() {
o(redirected).equals(true) o(redirected).equals(true)
done() done()
}, FRAME_BUDGET) }, FRAME_BUDGET)
}) })
o("onmatch resolution callback resolves at most once", function(done) { o("onmatch resolution callback resolves at most once", function(done) {
var resolveCount = 0 var resolveCount = 0
@ -456,7 +431,7 @@ o.spec("route", function() {
$window.location.href = prefix + "/" $window.location.href = prefix + "/"
route(root, "/", { route(root, "/", {
"/": { "/": {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
resolve(A) resolve(A)
resolve(B) resolve(B)
callAsync(function() {resolve(C)}) callAsync(function() {resolve(C)})
@ -475,14 +450,113 @@ o.spec("route", function() {
}, FRAME_BUDGET) }, FRAME_BUDGET)
}) })
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) { o("calling route.set invalidates pending onmatch resolution", function(done, timeout) {
timeout(100) timeout(50)
var resolved var resolved
$window.location.href = prefix + "/" $window.location.href = prefix + "/a"
route(root, "/a", { route(root, "/a", {
"/a": { "/a": {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
setTimeout(resolve, 20) setTimeout(resolve, 20)
}, },
render: function(vnode) {resolved = "a"} render: function(vnode) {resolved = "a"}
@ -491,15 +565,14 @@ o.spec("route", function() {
view: function() {resolved = "b"} view: function() {resolved = "b"}
} }
}) })
route.set("/b")
setTimeout(function() { setTimeout(function() {
route.set("/b") o(resolved).equals("b")
setTimeout(function() { done()
o(resolved).equals("b") }, 30)
done()
}, 30)
}, FRAME_BUDGET)
}) })
}) })
}) })

View file

@ -52,13 +52,13 @@ Argument | Type | Required | Description
##### route.get ##### 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()` `path = m.route.get()`
Argument | Type | Required | Description Argument | Type | Required | Description
----------------- | --------- | -------- | --- ----------------- | --------- | -------- | ---
**returns** | String | | Returns the current path **returns** | String | | Returns the last fully resolved path
##### route.prefix ##### route.prefix
@ -94,14 +94,12 @@ This method also allows you to asynchronously define what component will be rend
`routeResolver.onmatch(vnode, resolve)` `routeResolver.onmatch(vnode, resolve)`
Argument | Type | Description 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` `resolve` | `Function(Component)` | Call this function with a component as the first argument to use it as the route's component
`vnode.attrs` | `Object` | The [routing parameters](#routing-parameters) `args` | `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()` `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.
`vnode.attrs.route` | `String` | The matched route **returns** | | Returns `undefined`
`resolve` | `Function(Component)` | Call this function with a component as the first argument to use it as the route's component
**returns** | | Returns `undefined`
##### routeResolver.render ##### 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` | `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` | `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 **returns** | `Vnode` | Returns a vnode
--- ---
@ -266,12 +262,12 @@ m.route.prefix("/my-app")
### Advanced component resolution ### Advanced component resolution
Instead of mapping a component to a route, you can specify a RouteResolver object. A RouteResolver object contains a `onmatch()` method and a optionally a `render()` method. Instead of mapping a component to a route, you can specify a RouteResolver object. A RouteResolver object contains a `onmatch()` method and/or a `render()` method.
```javascript ```javascript
m.route(document.body, "/", { m.route(document.body, "/", {
"/": { "/": {
onmatch: function(vnode, resolve) { onmatch: function(resolve, args, requestedPath) {
resolve(Home) resolve(Home)
}, },
render: function(vnode) { render: function(vnode) {
@ -329,7 +325,7 @@ var Login = {
m.route(document.body, "/secret", { m.route(document.body, "/secret", {
"/secret": { "/secret": {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
if (isLoggedIn) resolve(Home) if (isLoggedIn) resolve(Home)
else m.route.set("/login") else m.route.set("/login")
}, },
@ -377,7 +373,7 @@ function load(file, done) {
m.route(document.body, "/", { m.route(document.body, "/", {
"/": { "/": {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
load("Home.js", resolve) load("Home.js", resolve)
}, },
}, },
@ -391,7 +387,7 @@ Fortunately, there are a number of tools that facilitate the task of bundling mo
```javascript ```javascript
m.route(document.body, "/", { m.route(document.body, "/", {
"/": { "/": {
onmatch: function(vnode, resolve) { onmatch: function(resolve) {
// using Webpack async code splitting // using Webpack async code splitting
require(['./Home.js'], resolve) require(['./Home.js'], resolve)
}, },

View file

@ -10,8 +10,8 @@ var Stream = require("./stream")
requestService.setCompletionCallback(redrawService.publish) requestService.setCompletionCallback(redrawService.publish)
m.route = require("./route")
m.mount = require("./mount") m.mount = require("./mount")
m.route = require("./route")
m.withAttr = require("./util/withAttr") m.withAttr = require("./util/withAttr")
m.prop = Stream m.prop = Stream
m.render = renderService.render m.render = renderService.render

View file

@ -1,4 +1,3 @@
var renderService = require("./render") var mount = require("./mount")
var redrawService = require("./redraw")
module.exports = require("./api/router")(window, renderService, redrawService) module.exports = require("./api/router")(window, mount)

View file

@ -16,6 +16,18 @@ module.exports = function($window) {
return 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) { function parsePath(path, queryData, hashData) {
var queryIndex = path.indexOf("?") var queryIndex = path.indexOf("?")
var hashIndex = path.indexOf("#") var hashIndex = path.indexOf("#")
@ -67,7 +79,7 @@ module.exports = function($window) {
} }
function defineRoutes(routes, resolve, reject) { function defineRoutes(routes, resolve, reject) {
if (supportsPushState) $window.onpopstate = resolveRoute if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute)
else if (prefix.charAt(0) === "#") $window.onhashchange = resolveRoute else if (prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
resolveRoute() resolveRoute()
@ -76,25 +88,23 @@ module.exports = function($window) {
var params = {} var params = {}
var pathname = parsePath(path, params, params) var pathname = parsePath(path, params, params)
callAsync(function() { for (var route in routes) {
for (var route in routes) { var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
if (matcher.test(pathname)) { if (matcher.test(pathname)) {
pathname.replace(matcher, function() { pathname.replace(matcher, function() {
var keys = route.match(/:[^\/]+/g) || [] var keys = route.match(/:[^\/]+/g) || []
var values = [].slice.call(arguments, 1, -2) var values = [].slice.call(arguments, 1, -2)
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
} }
resolve(routes[route], params, path, route) resolve(routes[route], params, path, route)
}) })
return return
}
} }
}
reject(path, params) reject(path, params)
})
} }
return resolveRoute return resolveRoute
} }

View file

@ -285,18 +285,14 @@ o.spec("Router.defineRoutes", function() {
}) })
}) })
o("replays", function(done) { o("replays", function() {
$window.location.href = prefix + "/test" $window.location.href = prefix + "/test"
var replay = router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) var replay = router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
replay() replay()
callAsync(function() { o(onRouteChange.callCount).equals(2)
o(onRouteChange.callCount).equals(2) o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"])
o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) o(onFail.callCount).equals(0)
o(onFail.callCount).equals(0)
done()
})
}) })
}) })
}) })

View file

@ -164,9 +164,11 @@ o.spec("api", function() {
setTimeout(function() { setTimeout(function() {
m.route.set("/b") m.route.set("/b")
o(m.route.get()).equals("/b") setTimeout(function() {
o(m.route.get()).equals("/b")
done() done()
}, FRAME_BUDGET)
}, FRAME_BUDGET) }, FRAME_BUDGET)
}) })
}) })