Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Last active July 8, 2018 09:07
Show Gist options
  • Save dead-claudia/90d6107b78d532307d64375e0675a935 to your computer and use it in GitHub Desktop.
Save dead-claudia/90d6107b78d532307d64375e0675a935 to your computer and use it in GitHub Desktop.
Mithril router concept
// mithril/route.js
"use strict"
var redrawService = require("./redraw")
var register = require("./router/register")(window)
module.exports = require("./router/router")(redrawService, register)
// mithril/router/register.js
"use strict"
module.exports = function($window) {
var supportsPushState = $window.history.pushState === "function"
return function(callback, prefixType) {
var skip
function resolveRoute() {
var path = ""
if (prefixType !== "hash") {
if (prefixType !== "query") path = $window.location.pathname
path += $window.location.search
}
path += $window.location.hash
var redirecting = path === skip
if (redirecting) skip = void 0
return callback(
path.replace(
/(?:%[a-f89][a-f0-9])+/gim,
$window.decodeURIComponent
),
supportsPushState ? $window.history.state : void 0,
redirecting
)
}
if (supportsPushState) $window.onpopstate = resolveRoute
else if (prefixType === "hash") $window.onhashchange = resolveRoute
resolveRoute()
return {
decode: $window.decodeURIComponent,
supportsState: supportsPushState,
unregister: function() {
if (supportsPushState) $window.onpopstate = void 0
else if (prefixType === "hash") $window.onhashchange = void 0
},
set: function(path, options, redirecting) {
if (!supportsPushState) {
if (path == null) {
$window.onhashchange = void 0
} else {
if (redirecting) skip = path
$window.location.href = path
}
} else if (path == null) {
$window.onpopstate = void 0
} else {
resolveRoute()
// Always replace when redirecting
if (redirecting) skip = path
if (redirecting || options.replace) {
$window.history.replaceState(
options.state, options.title, path
)
} else {
$window.history.pushState(
options.state, options.title, path
)
}
}
}
}
}
}
// mithril/router/router.js
"use strict"
var Promise = require("../promise/promise")
var m = require("../render/hyperscript")
var buildQueryString = require("../querystring/build")
var parseQueryString = require("../querystring/parse")
module.exports = function createRouter(redraw, register) {
var backend, path, params, prefix, routes, resolve, layout
var current, initialRoute, initialExec, bail
var hasOwn = {}.hasOwnProperty
var loading = false
var assign = Object.assign || function(target, source) {
for (var key in source) {
if (hasOwn.call(source, key)) target[key] = source[key]
}
}
function reset() {
if (backend != null) backend.unregister()
backend = path = params = prefix = routes = resolve = void 0
current = initialRoute = initialExec = bail = void 0
}
function decodeExec(exec) {
exec[0] = void 0
for (var i = 1; i < exec.length; i++) exec[i] = backend.decode(exec[i])
}
function resolveRoute(depth, current, options) {
var ignore = false
if (depth === 20) {
loading = false
throw new Error("Too many redirects!")
}
function render() {
if (ignore) return
if (depth !== 0) {
if (options.params) params = options.params
path = current
options.replace = true
backend.set(resolvePath(current, options), options, true)
}
loading = false
redraw()
}
function next(result) {
if (ignore) return
return result == null ? render() : resolveRoute(
depth + 1, result[0],
result[1] || {params: options.params}
)
}
var result = resolve(path, options.params)
if (result != null && typeof result.then === "function") {
Promise.resolve(result).then(next, render)
bail = function () { ignore = true }
} else {
bail = undefined
return next(result)
}
}
function update(route, exec, redirecting) {
for (var i = 1; i < exec.length; i++) params[route.keys[i]] = exec[i]
current = route
if (redirecting) return
loading = resolve != null
try {
if (loading) resolveRoute(0, path, {params: params})
} finally {
if (!loading) redraw()
}
}
function callback(newPath, state, redirecting) {
var newParams = Object.create(null)
var queryIndex = newPath.indexOf("?", prefix.length)
var hashIndex = newPath.indexOf("#", prefix.length)
if (queryIndex >= 0) {
assign(newParams, parseQueryString(hashIndex >= 0
? newPath.slice(queryIndex + 1, hashIndex)
: newPath.slice(queryIndex + 1)
))
}
if (hashIndex >= 0) {
assign(newParams, parseQueryString(newPath.slice(hashIndex + 1)))
}
if (state != null) assign(newParams, state)
newPath = queryIndex >= 0
? newPath.slice(prefix.length, queryIndex)
: newPath.slice(prefix.length)
if (newPath[0] !== "/") newPath = "/" + newPath
path = newPath
params = newParams
for (var i = 0; i < routes.length; i++) {
var exec = routes[i].regexp.exec(path)
if (exec != null) {
decodeExec(exec)
return update(routes[i], exec, redirecting)
}
}
return update(initialRoute, initialExec, redirecting)
}
function render(newParams) {
return (0, current.route)(newParams != null ? newParams : params)
}
function resolvePath(path, options) {
// Search for a query within the final hash if present, or the raw
// URL otherwise.
return prefix + path +
(path.indexOf("?", path.indexOf("#", prefix.length) + 1) < 0
? "?" : "&") +
buildQueryString(options.params)
}
return {
init: function(options) {
// For good measure.
reset()
resolve = options.resolve
prefix = options.prefix || "#!"
layout = options.layout
routes = Object.keys(options.routes).map(function(pattern) {
return {
regexp: new RegExp(
"^" +
pattern
.replace(/:[^\/]+?\.{3}/g, "(.*?)")
.replace(/:[^\/]+/g, "([^\\/]+)") +
"\/?$"
),
callback: options.routes[pattern],
// Start with padding, so it's easier to loop them when
// adding the exec result to the parameters.
keys: [void 0].concat((pattern.match(/:[^\/]+/g) || [])
.map(function(key) {
return key.replace(/:|\./g, "")
})
),
}
})
for (var i = 0; i < routes.length; i++) {
initialExec = routes[i].regexp.exec(options.initial)
if (initialExec != null) {
initialRoute = routes[i]
backend = register(callback,
prefix[0] === "#" ? "hash" :
prefix[0] === "?" ? "query" :
"path"
)
decodeExec(initialExec)
return {
view: function() {
if (layout == null) {
if (loading) return void 0
return (0, current.route)(params)
}
return layout(
loading ? void 0 : render,
path, params
)
},
onremove: reset,
}
}
}
reset()
throw new Error(
"Could not resolve default route " + options.initial
)
},
set: function(newPath, options) {
// Override existing routing when we go to a new route.
if (bail != null) { bail(); bail = void 0 }
if (options == null) options = {}
backend.set(resolvePath(newPath, options), options, false)
},
link: function() {
var options, href
var onclick = function(e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) {
return
}
e.preventDefault()
e.redraw = false
backend.set(href, options || {}, false)
}
return {
view: function(vnode) {
options = vnode.attrs
href = resolvePath(vnode.attrs.to, vnode.attrs)
var attrs = {href: href, onclick: onclick}
if (options.attrs) {
assign(attrs, options.attrs)
if ("tag" in attrs) attrs.tag = undefined
if ("attrs" in attrs) attrs.attrs = undefined
}
return m(vnode.attrs.tag || "a", attrs, vnode.children)
},
}
},
create: function(newRedraw, newRegister) {
return createRouter(newRedraw || redraw, newRegister || register)
},
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment