Deservicify core (#2458)
* De-servicify router (mostly) Still uses the redraw service, but it no longer has an intermediate service of its own. Also, did a *lot* of test deduplication in this. About 30-40% of the router service tests were already tested on the main router API instance itself. Bundle size decreased from 9560 to 9548 bytes min+gzip. * Merge `m.mount` + `m.redraw`, update router Simplifies the router and redraw mechanism, and makes it much easier to keep predictable. Bundle size down to 9433 bytes min+gzip, docs updated accordingly. * Make `mithril/render` just return the `m.render` function directly. * Deservicify `m.render`, revise `m.route` - You now have to use `mithril/render/render` directly if you want an implicit redraw function. (This will likely be going away in v3.) - Revise `m.route` to only `key` components * Add `redraw` to `m.render`, deservicify requests * Test error logging * Update docs + changelog [skip ci]
This commit is contained in:
parent
db277217f8
commit
1f4b2cf49a
60 changed files with 1212 additions and 1393 deletions
186
api/router.js
186
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)
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
function run() {
|
||||
init = true
|
||||
if (sentinel !== currentResolver) {
|
||||
var vnode = Vnode(component, attrs.key, attrs)
|
||||
if (currentResolver) vnode = currentResolver.render(vnode)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
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) })
|
||||
}
|
||||
else update(payload, "div")
|
||||
}
|
||||
}, bail, defaultRoute, function (unsubscribe) {
|
||||
redrawService.subscribe(root, function(sub) {
|
||||
sub.c = run
|
||||
return sub
|
||||
}, unsubscribe)
|
||||
},
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue