diff --git a/api/mount-redraw.js b/api/mount-redraw.js
new file mode 100644
index 000000000..d51da61b7
--- /dev/null
+++ b/api/mount-redraw.js
@@ -0,0 +1,50 @@
+"use strict"
+
+var Vnode = require("../render/vnode")
+
+module.exports = function(render, schedule, console) {
+ var subscriptions = []
+ var rendering = false
+ var pending = false
+
+ function sync() {
+ if (rendering) throw new Error("Nested m.redraw.sync() call")
+ rendering = true
+ for (var i = 0; i < subscriptions.length; i += 2) {
+ try { render(subscriptions[i], Vnode(subscriptions[i + 1]), redraw) }
+ catch (e) { console.error(e) }
+ }
+ rendering = false
+ }
+
+ function redraw() {
+ if (!pending) {
+ pending = true
+ schedule(function() {
+ pending = false
+ sync()
+ })
+ }
+ }
+
+ redraw.sync = sync
+
+ function mount(root, component) {
+ if (component != null && component.view == null && typeof component !== "function") {
+ throw new TypeError("m.mount(element, component) expects a component, not a vnode")
+ }
+
+ var index = subscriptions.indexOf(root)
+ if (index >= 0) {
+ subscriptions.splice(index, 2)
+ render(root, [], redraw)
+ }
+
+ if (component != null) {
+ subscriptions.push(root, component)
+ render(root, Vnode(component), redraw)
+ }
+ }
+
+ return {mount: mount, redraw: redraw}
+}
diff --git a/api/mount.js b/api/mount.js
deleted file mode 100644
index ab8ecc137..000000000
--- a/api/mount.js
+++ /dev/null
@@ -1,15 +0,0 @@
-"use strict"
-
-var Vnode = require("../render/vnode")
-
-module.exports = function(redrawService) {
- return function(root, component) {
- if (component === null) {
- redrawService.unsubscribe(root)
- } else if (component.view == null && typeof component !== "function") {
- throw new Error("m.mount(element, component) expects a component, not a vnode")
- } else {
- redrawService.subscribe(root, function() { return Vnode(component) })
- }
- }
-}
diff --git a/api/redraw.js b/api/redraw.js
deleted file mode 100644
index 235157f33..000000000
--- a/api/redraw.js
+++ /dev/null
@@ -1,58 +0,0 @@
-"use strict"
-
-var coreRenderer = require("../render/render")
-
-function throttle(callback) {
- var pending = null
- return function() {
- if (pending === null) {
- pending = requestAnimationFrame(function() {
- pending = null
- callback()
- })
- }
- }
-}
-
-module.exports = function($window, throttleMock) {
- var renderService = coreRenderer($window)
- var subscriptions = []
- var rendering = false
-
- function run(sub) {
- var vnode = sub.c(sub)
- if (vnode !== sub) renderService.render(sub.k, vnode)
- }
- function subscribe(key, callback, onremove) {
- var sub = {k: key, c: callback, r: onremove}
- unsubscribe(key)
- subscriptions.push(sub)
- var vnode = sub.c(sub)
- if (vnode !== sub) renderService.render(sub.k, vnode)
- }
- function unsubscribe(key) {
- for (var i = 0; i < subscriptions.length; i++) {
- var sub = subscriptions[i]
- if (sub.k === key) {
- subscriptions.splice(i, 1)
- renderService.render(sub.k, [])
- if (typeof sub.r === "function") sub.r()
- break
- }
- }
- }
- function sync() {
- if (rendering) throw new Error("Nested m.redraw.sync() call")
- rendering = true
- for (var i = 0; i < subscriptions.length; i++) {
- try { run(subscriptions[i]) }
- catch (e) { if (typeof console !== "undefined") console.error(e) }
- }
- rendering = false
- }
-
- var redraw = (throttleMock || throttle)(sync)
- redraw.sync = sync
- renderService.setRedraw(redraw)
- return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
-}
diff --git a/api/router.js b/api/router.js
index a388b40cf..3357e7f8c 100644
--- a/api/router.js
+++ b/api/router.js
@@ -2,55 +2,153 @@
var Vnode = require("../render/vnode")
var Promise = require("../promise/promise")
-var coreRouter = require("../router/router")
+
+var buildPathname = require("../pathname/build")
+var parsePathname = require("../pathname/parse")
+var compileTemplate = require("../pathname/compileTemplate")
+var assign = require("../pathname/assign")
var sentinel = {}
-module.exports = function($window, redrawService) {
- var routeService = coreRouter($window)
+module.exports = function($window, mountRedraw) {
+ var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
+ var supportsPushState = typeof $window.history.pushState === "function"
+ var routePrefix = "#!"
+ var fireAsync
+
+ function setPath(path, data, options) {
+ path = buildPathname(path, data)
+ if (fireAsync != null) {
+ fireAsync()
+ var state = options ? options.state : null
+ var title = options ? options.title : null
+ if (options && options.replace) $window.history.replaceState(state, title, routePrefix + path)
+ else $window.history.pushState(state, title, routePrefix + path)
+ }
+ else {
+ $window.location.href = routePrefix + path
+ }
+ }
var currentResolver = sentinel, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) {
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
- var init = false
- var bail = function(path) {
- if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
- else throw new Error("Could not resolve default route " + defaultRoute)
- }
- function run() {
- init = true
- if (sentinel !== currentResolver) {
- var vnode = Vnode(component, attrs.key, attrs)
- if (currentResolver) vnode = currentResolver.render(vnode)
- return vnode
+ // 0 = start
+ // 1 = init
+ // 2 = ready
+ var state = 0
+
+ var compiled = Object.keys(routes).map(function(route) {
+ if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`")
+ if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
+ throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
+ }
+ return {
+ route: route,
+ component: routes[route],
+ check: compileTemplate(route),
+ }
+ })
+ var onremove, asyncId
+
+ fireAsync = null
+
+ if (defaultRoute != null) {
+ var defaultData = parsePathname(defaultRoute)
+
+ if (!compiled.some(function (i) { return i.check(defaultData) })) {
+ throw new ReferenceError("Default route doesn't match any known routes")
}
}
- routeService.defineRoutes(routes, function(payload, params, path, route) {
- var update = lastUpdate = function(routeResolver, comp) {
- if (update !== lastUpdate) return
- component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
- attrs = params, currentPath = path, lastUpdate = null
- currentResolver = routeResolver.render ? routeResolver : null
- if (init) redrawService.redraw()
- else {
- init = true
- redrawService.redraw.sync()
+
+ function resolveRoute() {
+ // Consider the pathname holistically. The prefix might even be invalid,
+ // but that's not our problem.
+ var prefix = $window.location.hash
+ if (routePrefix[0] !== "#") {
+ prefix = $window.location.search + prefix
+ if (routePrefix[0] !== "?") {
+ prefix = $window.location.pathname + prefix
+ if (prefix[0] !== "/") prefix = "/" + prefix
}
}
- if (payload.view || typeof payload === "function") update({}, payload)
- else {
- if (payload.onmatch) {
- Promise.resolve(payload.onmatch(params, path, route)).then(function(resolved) {
- update(payload, resolved)
- }, function () { bail(path) })
+ // This seemingly useless `.concat()` speeds up the tests quite a bit,
+ // since the representation is consistently a relatively poorly
+ // optimized cons string.
+ var path = prefix.concat()
+ .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
+ .slice(routePrefix.length)
+ var data = parsePathname(path)
+
+ assign(data.params, $window.history.state)
+
+ for (var i = 0; i < compiled.length; i++) {
+ if (compiled[i].check(data)) {
+ var payload = compiled[i].component
+ var route = compiled[i].route
+ var update = lastUpdate = function(routeResolver, comp) {
+ if (update !== lastUpdate) return
+ component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
+ attrs = data.params, currentPath = path, lastUpdate = null
+ currentResolver = routeResolver.render ? routeResolver : null
+ if (state === 2) mountRedraw.redraw()
+ else {
+ state = 2
+ mountRedraw.redraw.sync()
+ }
+ }
+ if (payload.view || typeof payload === "function") update({}, payload)
+ else {
+ if (payload.onmatch) {
+ Promise.resolve(payload.onmatch(data.params, path, route)).then(function(resolved) {
+ update(payload, resolved)
+ }, function () {
+ if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
+ setPath(defaultRoute, null, {replace: true})
+ })
+ }
+ else update(payload, "div")
+ }
+ return
}
- else update(payload, "div")
}
- }, bail, defaultRoute, function (unsubscribe) {
- redrawService.subscribe(root, function(sub) {
- sub.c = run
- return sub
- }, unsubscribe)
+
+ if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
+ setPath(defaultRoute, null, {replace: true})
+ }
+
+ if (supportsPushState) {
+ onremove = function() {
+ $window.removeEventListener("popstate", fireAsync, false)
+ }
+ $window.addEventListener("popstate", fireAsync = function() {
+ if (asyncId) return
+ asyncId = callAsync(function() {
+ asyncId = null
+ resolveRoute()
+ })
+ }, false)
+ } else if (routePrefix[0] === "#") {
+ onremove = function() {
+ $window.removeEventListener("hashchange", resolveRoute, false)
+ }
+ $window.addEventListener("hashchange", resolveRoute, false)
+ }
+
+ return mountRedraw.mount(root, {
+ onbeforeupdate: function() {
+ state = state ? 2 : 1
+ return !(!state || sentinel === currentResolver)
+ },
+ oncreate: resolveRoute,
+ onremove: onremove,
+ view: function() {
+ if (!state || sentinel === currentResolver) return
+ // Wrap in a fragment to preserve existing key semantics
+ var vnode = [Vnode(component, attrs.key, attrs)]
+ if (currentResolver) vnode = currentResolver.render(vnode[0])
+ return vnode
+ },
})
}
route.set = function(path, data, options) {
@@ -59,18 +157,18 @@ module.exports = function($window, redrawService) {
options.replace = true
}
lastUpdate = null
- routeService.setPath(path, data, options)
+ setPath(path, data, options)
}
route.get = function() {return currentPath}
- route.prefix = function(prefix) {routeService.prefix = prefix}
+ route.prefix = function(prefix) {routePrefix = prefix}
var link = function(options, vnode) {
- vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href)
+ vnode.dom.setAttribute("href", routePrefix + vnode.attrs.href)
vnode.dom.onclick = function(e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
e.preventDefault()
e.redraw = false
var href = this.getAttribute("href")
- if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
+ if (href.indexOf(routePrefix) === 0) href = href.slice(routePrefix.length)
route.set(href, undefined, options)
}
}
diff --git a/api/tests/index.html b/api/tests/index.html
index e85bd4b7e..e7d28bdbc 100644
--- a/api/tests/index.html
+++ b/api/tests/index.html
@@ -24,13 +24,11 @@
-
-
-
+
-
-
+
+