Deduplicate m.route and m.redraw logic (#2453)
- Remove appropriate route change subcriptions when a root is removed via `m.mount(root, null)`. - Don't pollute `onpopstate` and friends - use standard event listeners instead. - Simplify and streamline subscriptions, in preparation of adding a `remove` parameter to `m.mount`. - Change the redraw internals to redraw immediately, with ability to cancel via returning a sentinel. - Change `"bleeding-edge"` for `m.version` in `next` to instead just be the latest `m.version`. (If you're using `next`, you should know what you're in for.) - Update tests to be aware of these changes. (Some were failing for subtle reasons.) - Drive-by: remove some uses of `string.charAt(n)` and use `string[n]` instead.
This commit is contained in:
parent
6c562d2b9b
commit
90bcff0fa7
18 changed files with 397 additions and 192 deletions
16
api/mount.js
16
api/mount.js
|
|
@ -5,17 +5,11 @@ var Vnode = require("../render/vnode")
|
||||||
module.exports = function(redrawService) {
|
module.exports = function(redrawService) {
|
||||||
return function(root, component) {
|
return function(root, component) {
|
||||||
if (component === null) {
|
if (component === null) {
|
||||||
redrawService.render(root, [])
|
|
||||||
redrawService.unsubscribe(root)
|
redrawService.unsubscribe(root)
|
||||||
return
|
} else if (component.view == null && typeof component !== "function") {
|
||||||
}
|
throw new Error("m.mount(element, component) expects a component, not a vnode")
|
||||||
|
} else {
|
||||||
if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode")
|
redrawService.subscribe(root, function() { return Vnode(component) })
|
||||||
|
}
|
||||||
var run = function() {
|
|
||||||
redrawService.render(root, Vnode(component))
|
|
||||||
}
|
|
||||||
redrawService.subscribe(root, run)
|
|
||||||
run()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,40 @@ function throttle(callback) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = function($window, throttleMock) {
|
module.exports = function($window, throttleMock) {
|
||||||
var renderService = coreRenderer($window)
|
var renderService = coreRenderer($window)
|
||||||
var callbacks = []
|
var subscriptions = []
|
||||||
var rendering = false
|
var rendering = false
|
||||||
|
|
||||||
function subscribe(key, callback) {
|
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)
|
unsubscribe(key)
|
||||||
callbacks.push(key, callback)
|
subscriptions.push(sub)
|
||||||
|
var vnode = sub.c(sub)
|
||||||
|
if (vnode !== sub) renderService.render(sub.k, vnode)
|
||||||
}
|
}
|
||||||
function unsubscribe(key) {
|
function unsubscribe(key) {
|
||||||
var index = callbacks.indexOf(key)
|
for (var i = 0; i < subscriptions.length; i++) {
|
||||||
if (index > -1) callbacks.splice(index, 2)
|
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() {
|
function sync() {
|
||||||
if (rendering) throw new Error("Nested m.redraw.sync() call")
|
if (rendering) throw new Error("Nested m.redraw.sync() call")
|
||||||
rendering = true
|
rendering = true
|
||||||
for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)}
|
for (var i = 0; i < subscriptions.length; i++) {
|
||||||
|
try { run(subscriptions[i]) }
|
||||||
|
catch (e) { if (typeof console !== "undefined") console.error(e) }
|
||||||
|
}
|
||||||
rendering = false
|
rendering = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,38 @@ var Vnode = require("../render/vnode")
|
||||||
var Promise = require("../promise/promise")
|
var Promise = require("../promise/promise")
|
||||||
var coreRouter = require("../router/router")
|
var coreRouter = require("../router/router")
|
||||||
|
|
||||||
|
var sentinel = {}
|
||||||
|
|
||||||
module.exports = function($window, redrawService) {
|
module.exports = function($window, redrawService) {
|
||||||
var routeService = coreRouter($window)
|
var routeService = coreRouter($window)
|
||||||
|
|
||||||
var identity = function(v) {return v}
|
var currentResolver = sentinel, component, attrs, currentPath, lastUpdate
|
||||||
var render, component, attrs, currentPath, lastUpdate
|
|
||||||
var route = function(root, defaultRoute, routes) {
|
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")
|
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
|
||||||
function run() {
|
var init = false
|
||||||
if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs)))
|
|
||||||
}
|
|
||||||
var redraw = function() {
|
|
||||||
run()
|
|
||||||
redraw = redrawService.redraw
|
|
||||||
}
|
|
||||||
redrawService.subscribe(root, run)
|
|
||||||
var bail = function(path) {
|
var bail = function(path) {
|
||||||
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
|
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
|
||||||
else throw new Error("Could not resolve default route " + defaultRoute)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
routeService.defineRoutes(routes, function(payload, params, path, route) {
|
routeService.defineRoutes(routes, function(payload, params, path, route) {
|
||||||
var update = lastUpdate = function(routeResolver, comp) {
|
var update = lastUpdate = function(routeResolver, comp) {
|
||||||
if (update !== lastUpdate) return
|
if (update !== lastUpdate) return
|
||||||
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
|
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
|
||||||
attrs = params, currentPath = path, lastUpdate = null
|
attrs = params, currentPath = path, lastUpdate = null
|
||||||
render = (routeResolver.render || identity).bind(routeResolver)
|
currentResolver = routeResolver.render ? routeResolver : null
|
||||||
redraw()
|
if (init) redrawService.redraw()
|
||||||
|
else {
|
||||||
|
init = true
|
||||||
|
redrawService.redraw.sync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (payload.view || typeof payload === "function") update({}, payload)
|
if (payload.view || typeof payload === "function") update({}, payload)
|
||||||
else {
|
else {
|
||||||
|
|
@ -40,7 +46,12 @@ module.exports = function($window, redrawService) {
|
||||||
}
|
}
|
||||||
else update(payload, "div")
|
else update(payload, "div")
|
||||||
}
|
}
|
||||||
}, bail, defaultRoute)
|
}, bail, defaultRoute, function (unsubscribe) {
|
||||||
|
redrawService.subscribe(root, function(sub) {
|
||||||
|
sub.c = run
|
||||||
|
return sub
|
||||||
|
}, unsubscribe)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
route.set = function(path, data, options) {
|
route.set = function(path, data, options) {
|
||||||
if (lastUpdate != null) {
|
if (lastUpdate != null) {
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,15 @@ o.spec("redrawService", function() {
|
||||||
|
|
||||||
redrawService.subscribe(root, spy)
|
redrawService.subscribe(root, spy)
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
redrawService.redraw()
|
redrawService.redraw()
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
throttleMock.fire()
|
throttleMock.fire()
|
||||||
|
|
||||||
o(spy.callCount).equals(1)
|
o(spy.callCount).equals(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
o("should run a single renderer entry", function(done) {
|
o("should run a single renderer entry", function(done) {
|
||||||
|
|
@ -54,16 +54,16 @@ o.spec("redrawService", function() {
|
||||||
|
|
||||||
redrawService.subscribe(root, spy)
|
redrawService.subscribe(root, spy)
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
|
||||||
|
|
||||||
redrawService.redraw()
|
|
||||||
redrawService.redraw()
|
|
||||||
redrawService.redraw()
|
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
|
||||||
setTimeout(function() {
|
|
||||||
o(spy.callCount).equals(1)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
|
redrawService.redraw()
|
||||||
|
redrawService.redraw()
|
||||||
|
redrawService.redraw()
|
||||||
|
|
||||||
|
o(spy.callCount).equals(1)
|
||||||
|
setTimeout(function() {
|
||||||
|
o(spy.callCount).equals(2)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}, 20)
|
}, 20)
|
||||||
})
|
})
|
||||||
|
|
@ -82,57 +82,67 @@ o.spec("redrawService", function() {
|
||||||
|
|
||||||
redrawService.redraw()
|
redrawService.redraw()
|
||||||
|
|
||||||
o(spy1.callCount).equals(0)
|
|
||||||
o(spy2.callCount).equals(0)
|
|
||||||
o(spy3.callCount).equals(0)
|
|
||||||
|
|
||||||
redrawService.redraw()
|
|
||||||
|
|
||||||
o(spy1.callCount).equals(0)
|
|
||||||
o(spy2.callCount).equals(0)
|
|
||||||
o(spy3.callCount).equals(0)
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
o(spy1.callCount).equals(1)
|
o(spy1.callCount).equals(1)
|
||||||
o(spy2.callCount).equals(1)
|
o(spy2.callCount).equals(1)
|
||||||
o(spy3.callCount).equals(1)
|
o(spy3.callCount).equals(1)
|
||||||
|
|
||||||
|
redrawService.redraw()
|
||||||
|
|
||||||
|
o(spy1.callCount).equals(1)
|
||||||
|
o(spy2.callCount).equals(1)
|
||||||
|
o(spy3.callCount).equals(1)
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
o(spy1.callCount).equals(2)
|
||||||
|
o(spy2.callCount).equals(2)
|
||||||
|
o(spy3.callCount).equals(2)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}, 20)
|
}, 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
o("should stop running after unsubscribe", function(done) {
|
o("should stop running after unsubscribe", function(done) {
|
||||||
var spy = o.spy(function() {
|
var spy = o.spy()
|
||||||
throw new Error("This shouldn't have been called")
|
|
||||||
})
|
|
||||||
|
|
||||||
redrawService.subscribe(root, spy)
|
redrawService.subscribe(root, spy)
|
||||||
redrawService.unsubscribe(root, spy)
|
o(spy.callCount).equals(1)
|
||||||
|
redrawService.unsubscribe(root)
|
||||||
|
|
||||||
redrawService.redraw()
|
redrawService.redraw()
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}, 20)
|
}, 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
|
o("should invoke remove callback on unsubscribe", function() {
|
||||||
var spy = o.spy(function() {
|
var spy = o.spy()
|
||||||
throw new Error("This shouldn't have been called")
|
var onremove = o.spy()
|
||||||
|
|
||||||
|
redrawService.subscribe(root, spy, onremove)
|
||||||
|
o(spy.callCount).equals(1)
|
||||||
|
redrawService.unsubscribe(root)
|
||||||
|
|
||||||
|
o(spy.callCount).equals(1)
|
||||||
|
o(onremove.callCount).equals(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
|
||||||
|
var spy = o.spy()
|
||||||
|
|
||||||
redrawService.subscribe(root, spy)
|
redrawService.subscribe(root, spy)
|
||||||
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
redrawService.redraw()
|
redrawService.redraw()
|
||||||
|
|
||||||
redrawService.unsubscribe(root, spy)
|
redrawService.unsubscribe(root)
|
||||||
|
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
o(spy.callCount).equals(0)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}, 20)
|
}, 20)
|
||||||
|
|
@ -142,12 +152,13 @@ o.spec("redrawService", function() {
|
||||||
var spy = o.spy()
|
var spy = o.spy()
|
||||||
|
|
||||||
redrawService.subscribe(root, spy)
|
redrawService.subscribe(root, spy)
|
||||||
redrawService.unsubscribe(null)
|
o(spy.callCount).equals(1)
|
||||||
|
|
||||||
|
redrawService.unsubscribe(null)
|
||||||
redrawService.redraw()
|
redrawService.redraw()
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
o(spy.callCount).equals(1)
|
o(spy.callCount).equals(2)
|
||||||
|
|
||||||
done()
|
done()
|
||||||
}, 20)
|
}, 20)
|
||||||
|
|
@ -165,12 +176,6 @@ o.spec("redrawService", function() {
|
||||||
redrawService.subscribe(el2, spy2)
|
redrawService.subscribe(el2, spy2)
|
||||||
redrawService.subscribe(el3, spy3)
|
redrawService.subscribe(el3, spy3)
|
||||||
|
|
||||||
o(spy1.callCount).equals(0)
|
|
||||||
o(spy2.callCount).equals(0)
|
|
||||||
o(spy3.callCount).equals(0)
|
|
||||||
|
|
||||||
redrawService.redraw.sync()
|
|
||||||
|
|
||||||
o(spy1.callCount).equals(1)
|
o(spy1.callCount).equals(1)
|
||||||
o(spy2.callCount).equals(1)
|
o(spy2.callCount).equals(1)
|
||||||
o(spy3.callCount).equals(1)
|
o(spy3.callCount).equals(1)
|
||||||
|
|
@ -180,5 +185,11 @@ o.spec("redrawService", function() {
|
||||||
o(spy1.callCount).equals(2)
|
o(spy1.callCount).equals(2)
|
||||||
o(spy2.callCount).equals(2)
|
o(spy2.callCount).equals(2)
|
||||||
o(spy3.callCount).equals(2)
|
o(spy3.callCount).equals(2)
|
||||||
|
|
||||||
|
redrawService.redraw.sync()
|
||||||
|
|
||||||
|
o(spy1.callCount).equals(3)
|
||||||
|
o(spy2.callCount).equals(3)
|
||||||
|
o(spy3.callCount).equals(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,25 @@ o.spec("route", function() {
|
||||||
o(view.callCount).equals(2)
|
o(view.callCount).equals(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
o("subscribes correctly and removes when unmounted", function() {
|
||||||
|
$window.location.href = prefix + "/"
|
||||||
|
|
||||||
|
route(root, "/", {
|
||||||
|
"/" : {
|
||||||
|
view: function() {
|
||||||
|
return m("div")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
o(root.firstChild.nodeName).equals("DIV")
|
||||||
|
|
||||||
|
// unsubscribe as if via `m.mount(root)`
|
||||||
|
redrawService.unsubscribe(root)
|
||||||
|
|
||||||
|
o(root.childNodes.length).equals(0)
|
||||||
|
})
|
||||||
|
|
||||||
o("default route doesn't break back button", function(done) {
|
o("default route doesn't break back button", function(done) {
|
||||||
$window.location.href = "http://old.com"
|
$window.location.href = "http://old.com"
|
||||||
$window.location.href = "http://new.com"
|
$window.location.href = "http://new.com"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ function parse(file) {
|
||||||
try {return JSON.parse(json)} catch (e) {throw new Error("invalid JSON: " + json)}
|
try {return JSON.parse(json)} catch (e) {throw new Error("invalid JSON: " + json)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pkg = require("../package.json")
|
||||||
var error
|
var error
|
||||||
module.exports = function (input) {
|
module.exports = function (input) {
|
||||||
var modules = {}
|
var modules = {}
|
||||||
|
|
@ -23,6 +24,10 @@ module.exports = function (input) {
|
||||||
var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
|
var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
|
||||||
var uuid = 0
|
var uuid = 0
|
||||||
var process = function(filepath, data) {
|
var process = function(filepath, data) {
|
||||||
|
// HACK: inline Mithril's `package.json` keys without reading the whole file.
|
||||||
|
data = data.replace(/require\((['"])\.\/package\.json\1\)\.(\w+)/, function (match, quote, key) {
|
||||||
|
return JSON.stringify(pkg[key])
|
||||||
|
})
|
||||||
data.replace(declaration, function(match, binding) {bindings[binding] = 0})
|
data.replace(declaration, function(match, binding) {bindings[binding] = 0})
|
||||||
|
|
||||||
return data.replace(include, function(match, def, variable, eq, dep, rest) {
|
return data.replace(include, function(match, def, variable, eq, dep, rest) {
|
||||||
|
|
@ -106,13 +111,10 @@ module.exports = function (input) {
|
||||||
+ (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
|
+ (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
var versionTag = "bleeding-edge"
|
|
||||||
var packageFile = __dirname + "/../package.json"
|
|
||||||
var code = process(path.resolve(input), read(input))
|
var code = process(path.resolve(input), read(input))
|
||||||
.replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
|
.replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
|
||||||
.replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
|
.replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
|
||||||
.replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
|
.replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
|
||||||
.replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
|
|
||||||
|
|
||||||
code = ";(function() {\n" + code + "\n}());"
|
code = ";(function() {\n" + code + "\n}());"
|
||||||
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
|
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ var o = require("../../ospec/ospec")
|
||||||
var bundle = require("../bundle")
|
var bundle = require("../bundle")
|
||||||
|
|
||||||
var fs = require("fs")
|
var fs = require("fs")
|
||||||
|
var pkg = require("../../package.json")
|
||||||
|
|
||||||
var ns = "./"
|
var ns = "./"
|
||||||
function write(filepath, data) {
|
function write(filepath, data) {
|
||||||
|
|
@ -319,4 +320,11 @@ o.spec("bundler", function() {
|
||||||
remove("a.js")
|
remove("a.js")
|
||||||
remove("b.js")
|
remove("b.js")
|
||||||
})
|
})
|
||||||
|
o("reads package.json keys", function() {
|
||||||
|
write("a.js", 'var b = require("./package.json").version')
|
||||||
|
|
||||||
|
o(bundle(ns + "a.js")).equals(";(function() {\nvar b = " + JSON.stringify(pkg.version) + "\n}());")
|
||||||
|
|
||||||
|
remove("a.js")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@
|
||||||
- route: Declared routes in `m.route` now support `-` and `.` as delimiters for path segments. This means you can have a route like `"/edit/:file.:ext"`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
- route: Declared routes in `m.route` now support `-` and `.` as delimiters for path segments. This means you can have a route like `"/edit/:file.:ext"`. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||||
- Previously, this was possible to do in `m.route.set`, `m.request`, and `m.jsonp`, but it was wholly untested for and also undocumented.
|
- Previously, this was possible to do in `m.route.set`, `m.request`, and `m.jsonp`, but it was wholly untested for and also undocumented.
|
||||||
- API: `m.buildPathname` and `m.parsePathname` added. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
- API: `m.buildPathname` and `m.parsePathname` added. ([#2361](https://github.com/MithrilJS/mithril.js/pull/2361))
|
||||||
|
- route: Use `m.mount(root, null)` to unsubscribe and clean up after a `m.route(root, ...)` call. ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453))
|
||||||
|
- version: `m.version` returns the previous version string for what's in `next`. ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453))
|
||||||
|
- If you're using `next`, you should hopefully know what you're doing. If you need stability, don't use `next`. (This is also why I'm not labelling it as a breaking change.)
|
||||||
|
|
||||||
#### Bug fixes
|
#### Bug fixes
|
||||||
|
|
||||||
|
|
@ -111,6 +114,7 @@
|
||||||
- request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@isiahmeadows](https://github.com/isiahmeadows))
|
- request: correct IE workaround for response type non-support ([#2449](https://github.com/MithrilJS/mithril.js/pull/2449) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
- render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@isiahmeadows](https://github.com/isiahmeadows))
|
- render: correct `contenteditable` check to also check for `contentEditable` property name ([#2450](https://github.com/MithrilJS/mithril.js/pull/2450) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
- docs: clarify valid key usage ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
|
- docs: clarify valid key usage ([#2452](https://github.com/MithrilJS/mithril.js/pull/2452) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
|
- route: don't pollute globals ([#2453](https://github.com/MithrilJS/mithril.js/pull/2453) [@isiahmeadows](https://github.com/isiahmeadows))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
- [Authentication](#authentication)
|
- [Authentication](#authentication)
|
||||||
- [Preloading data](#preloading-data)
|
- [Preloading data](#preloading-data)
|
||||||
- [Code splitting](#code-splitting)
|
- [Code splitting](#code-splitting)
|
||||||
|
- [Third-party integration](#third-party-integration)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -730,3 +731,81 @@ m.route(document.body, "/", {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Third-party integration
|
||||||
|
|
||||||
|
In certain situations, you may find yourself needing to interoperate with another framework like React. Here's how you do it:
|
||||||
|
|
||||||
|
- Define all your routes using `m.route` as normal, but make sure you only use it *once*. Multiple route points are not supported.
|
||||||
|
- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on.
|
||||||
|
|
||||||
|
Here's an example with React:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Child extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.root = React.createRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
m.route(this.root, "/", {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUnmount() {
|
||||||
|
m.mount(this.root, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div ref={this.root} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's the rough equivalent with Vue:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div ref="root"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Vue.component("my-child", {
|
||||||
|
template: `<div ref="root"></div>`,
|
||||||
|
mounted: function() {
|
||||||
|
m.route(this.$refs.root, "/", {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroyed: function() {
|
||||||
|
m.mount(this.$refs.root, null)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Technically, there's nothing stopping you from even doing it in a Mithril component, even.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Don't do this. Use a proper global layout component for each route instead,
|
||||||
|
// passing your child vnode/component in the attributes or children.
|
||||||
|
function Child() {
|
||||||
|
return {
|
||||||
|
oncreate: function(vnode) {
|
||||||
|
m.route(vnode.dom, "/", {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onremove: function() {
|
||||||
|
m.mount(vnode.dom, null)
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function() {
|
||||||
|
return m("div")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
||||||
2
index.js
2
index.js
|
|
@ -21,7 +21,7 @@ m.parseQueryString = require("./querystring/parse")
|
||||||
m.buildQueryString = require("./querystring/build")
|
m.buildQueryString = require("./querystring/build")
|
||||||
m.parsePathname = require("./pathname/parse")
|
m.parsePathname = require("./pathname/parse")
|
||||||
m.buildPathname = require("./pathname/build")
|
m.buildPathname = require("./pathname/build")
|
||||||
m.version = "bleeding-edge"
|
m.version = require("./package.json").version
|
||||||
m.vnode = require("./render/vnode")
|
m.vnode = require("./render/vnode")
|
||||||
m.PromisePolyfill = require("./promise/polyfill")
|
m.PromisePolyfill = require("./promise/polyfill")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ var o = require("../../ospec/ospec")
|
||||||
var callAsync = require("../../test-utils/callAsync")
|
var callAsync = require("../../test-utils/callAsync")
|
||||||
var xhrMock = require("../../test-utils/xhrMock")
|
var xhrMock = require("../../test-utils/xhrMock")
|
||||||
var Request = require("../../request/request")
|
var Request = require("../../request/request")
|
||||||
var Promise = require("../../promise/promise")
|
var PromisePolyfill = require("../../promise/promise")
|
||||||
|
|
||||||
o.spec("request", function() {
|
o.spec("request", function() {
|
||||||
var mock, request, complete
|
var mock, request, complete
|
||||||
o.beforeEach(function() {
|
o.beforeEach(function() {
|
||||||
mock = xhrMock()
|
mock = xhrMock()
|
||||||
var requestService = Request(mock, Promise)
|
var requestService = Request(mock, PromisePolyfill)
|
||||||
request = requestService.request
|
request = requestService.request
|
||||||
complete = o.spy()
|
complete = o.spy()
|
||||||
requestService.setCompletionCallback(complete)
|
requestService.setCompletionCallback(complete)
|
||||||
|
|
@ -587,6 +587,7 @@ o.spec("request", function() {
|
||||||
promise.then(then, catch2)
|
promise.then(then, catch2)
|
||||||
promise.then(then).catch(catch3)
|
promise.then(then).catch(catch3)
|
||||||
|
|
||||||
|
callAsync(function() {
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(catch1.callCount).equals(1)
|
o(catch1.callCount).equals(1)
|
||||||
|
|
@ -597,6 +598,7 @@ o.spec("request", function() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
o("rejects on cors-like error", function(done) {
|
o("rejects on cors-like error", function(done) {
|
||||||
mock.$defineRoutes({
|
mock.$defineRoutes({
|
||||||
"GET /item": function() {
|
"GET /item": function() {
|
||||||
|
|
@ -759,6 +761,14 @@ o.spec("request", function() {
|
||||||
)
|
)
|
||||||
|
|
||||||
o("invokes the redraw in native async/await", function () {
|
o("invokes the redraw in native async/await", function () {
|
||||||
|
// Use the native promise for correct semantics. This test will fail
|
||||||
|
// if you use the polyfill, as it's based on `setImmediate` (falling
|
||||||
|
// back to `setTimeout`), and promise microtasks are run at higher
|
||||||
|
// priority than either of those.
|
||||||
|
var requestService = Request(mock, Promise)
|
||||||
|
request = requestService.request
|
||||||
|
complete = o.spy()
|
||||||
|
requestService.setCompletionCallback(complete)
|
||||||
mock.$defineRoutes({
|
mock.$defineRoutes({
|
||||||
"GET /item": function() {
|
"GET /item": function() {
|
||||||
return {status: 200, responseText: "[]"}
|
return {status: 200, responseText: "[]"}
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,20 @@ var compileTemplate = require("../pathname/compileTemplate")
|
||||||
var assign = require("../pathname/assign")
|
var assign = require("../pathname/assign")
|
||||||
|
|
||||||
module.exports = function($window) {
|
module.exports = function($window) {
|
||||||
var supportsPushState = typeof $window.history.pushState === "function"
|
|
||||||
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
|
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
|
||||||
|
var supportsPushState = typeof $window.history.pushState === "function"
|
||||||
|
var fireAsync
|
||||||
|
|
||||||
var asyncId
|
return {
|
||||||
var router = {prefix: "#!"}
|
prefix: "#!",
|
||||||
router.getPath = function() {
|
|
||||||
|
getPath: function() {
|
||||||
// Consider the pathname holistically. The prefix might even be invalid,
|
// Consider the pathname holistically. The prefix might even be invalid,
|
||||||
// but that's not our problem.
|
// but that's not our problem.
|
||||||
var prefix = $window.location.hash
|
var prefix = $window.location.hash
|
||||||
if (router.prefix[0] !== "#") {
|
if (this.prefix[0] !== "#") {
|
||||||
prefix = $window.location.search + prefix
|
prefix = $window.location.search + prefix
|
||||||
if (router.prefix[0] !== "?") {
|
if (this.prefix[0] !== "?") {
|
||||||
prefix = $window.location.pathname + prefix
|
prefix = $window.location.pathname + prefix
|
||||||
if (prefix[0] !== "/") prefix = "/" + prefix
|
if (prefix[0] !== "/") prefix = "/" + prefix
|
||||||
}
|
}
|
||||||
|
|
@ -27,24 +29,27 @@ module.exports = function($window) {
|
||||||
// optimized cons string.
|
// optimized cons string.
|
||||||
return prefix.concat()
|
return prefix.concat()
|
||||||
.replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
|
.replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
|
||||||
.slice(router.prefix.length)
|
.slice(this.prefix.length)
|
||||||
}
|
},
|
||||||
|
|
||||||
router.setPath = function(path, data, options) {
|
setPath: function(path, data, options) {
|
||||||
path = buildPathname(path, data)
|
path = buildPathname(path, data)
|
||||||
if (supportsPushState) {
|
if (fireAsync != null) {
|
||||||
|
fireAsync()
|
||||||
var state = options ? options.state : null
|
var state = options ? options.state : null
|
||||||
var title = options ? options.title : null
|
var title = options ? options.title : null
|
||||||
$window.onpopstate()
|
if (options && options.replace) $window.history.replaceState(state, title, this.prefix + path)
|
||||||
if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path)
|
else $window.history.pushState(state, title, this.prefix + path)
|
||||||
else $window.history.pushState(state, title, router.prefix + path)
|
|
||||||
}
|
}
|
||||||
else $window.location.href = router.prefix + path
|
else {
|
||||||
|
$window.location.href = this.prefix + path
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
router.defineRoutes = function(routes, resolve, reject, defaultRoute) {
|
defineRoutes: function(routes, resolve, reject, defaultRoute, subscribe) {
|
||||||
|
var self = this
|
||||||
var compiled = Object.keys(routes).map(function(route) {
|
var compiled = Object.keys(routes).map(function(route) {
|
||||||
if (route.charAt(0) !== "/") throw new SyntaxError("Routes must start with a `/`")
|
if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`")
|
||||||
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
|
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
|
||||||
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
|
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +59,9 @@ module.exports = function($window) {
|
||||||
check: compileTemplate(route),
|
check: compileTemplate(route),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
var unsubscribe, asyncId
|
||||||
|
|
||||||
|
fireAsync = null
|
||||||
|
|
||||||
if (defaultRoute != null) {
|
if (defaultRoute != null) {
|
||||||
var defaultData = parsePathname(defaultRoute)
|
var defaultData = parsePathname(defaultRoute)
|
||||||
|
|
@ -64,7 +72,7 @@ module.exports = function($window) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRoute() {
|
function resolveRoute() {
|
||||||
var path = router.getPath()
|
var path = self.getPath()
|
||||||
var data = parsePathname(path)
|
var data = parsePathname(path)
|
||||||
|
|
||||||
assign(data.params, $window.history.state)
|
assign(data.params, $window.history.state)
|
||||||
|
|
@ -80,17 +88,25 @@ module.exports = function($window) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportsPushState) {
|
if (supportsPushState) {
|
||||||
$window.onpopstate = function() {
|
unsubscribe = function() {
|
||||||
|
$window.removeEventListener("popstate", fireAsync, false)
|
||||||
|
}
|
||||||
|
$window.addEventListener("popstate", fireAsync = function() {
|
||||||
if (asyncId) return
|
if (asyncId) return
|
||||||
asyncId = callAsync(function() {
|
asyncId = callAsync(function() {
|
||||||
asyncId = null
|
asyncId = null
|
||||||
resolveRoute()
|
resolveRoute()
|
||||||
})
|
})
|
||||||
|
}, false)
|
||||||
|
} else if (this.prefix[0] === "#") {
|
||||||
|
unsubscribe = function() {
|
||||||
|
$window.removeEventListener("hashchange", resolveRoute, false)
|
||||||
}
|
}
|
||||||
}
|
$window.addEventListener("hashchange", resolveRoute, false)
|
||||||
else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
|
|
||||||
resolveRoute()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
subscribe(unsubscribe)
|
||||||
|
resolveRoute()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ o.spec("Router.defineRoutes", function() {
|
||||||
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 $window, router, onRouteChange, onFail
|
var $window, router, onRouteChange, onFail
|
||||||
|
|
||||||
|
function defineRoutes(routes, defaultRoute) {
|
||||||
|
router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {})
|
||||||
|
}
|
||||||
|
|
||||||
o.beforeEach(function() {
|
o.beforeEach(function() {
|
||||||
$window = pushStateMock(env)
|
$window = pushStateMock(env)
|
||||||
router = new Router($window)
|
router = new Router($window)
|
||||||
|
|
@ -21,7 +25,10 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("calls onRouteChange on init", function(done) {
|
o("calls onRouteChange on init", function(done) {
|
||||||
$window.location.href = prefix + "/a"
|
$window.location.href = prefix + "/a"
|
||||||
router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail)
|
var subscribe = o.spy()
|
||||||
|
|
||||||
|
router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail, null, subscribe)
|
||||||
|
o(subscribe.callCount).equals(1)
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -32,7 +39,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("resolves to route", function(done) {
|
o("resolves to route", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -45,7 +52,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("resolves to route w/ escaped unicode", function(done) {
|
o("resolves to route w/ escaped unicode", function(done) {
|
||||||
$window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6"
|
$window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6"
|
||||||
router.defineRoutes({"/ö": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/ö": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -58,7 +65,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("resolves to route w/ unicode", function(done) {
|
o("resolves to route w/ unicode", function(done) {
|
||||||
$window.location.href = prefix + "/ö?ö=ö"
|
$window.location.href = prefix + "/ö?ö=ö"
|
||||||
router.defineRoutes({"/ö": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/ö": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -75,7 +82,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
router = new Router($window)
|
router = new Router($window)
|
||||||
router.prefix = prefix
|
router.prefix = prefix
|
||||||
|
|
||||||
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -88,7 +95,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles parameterized route", function(done) {
|
o("handles parameterized route", function(done) {
|
||||||
$window.location.href = prefix + "/test/x"
|
$window.location.href = prefix + "/test/x"
|
||||||
router.defineRoutes({"/test/:a": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test/:a": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -101,7 +108,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles multi-parameterized route", function(done) {
|
o("handles multi-parameterized route", function(done) {
|
||||||
$window.location.href = prefix + "/test/x/y"
|
$window.location.href = prefix + "/test/x/y"
|
||||||
router.defineRoutes({"/test/:a/:b": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test/:a/:b": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -114,7 +121,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles rest parameterized route", function(done) {
|
o("handles rest parameterized route", function(done) {
|
||||||
$window.location.href = prefix + "/test/x/y"
|
$window.location.href = prefix + "/test/x/y"
|
||||||
router.defineRoutes({"/test/:a...": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test/:a...": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -127,7 +134,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles route with search", function(done) {
|
o("handles route with search", function(done) {
|
||||||
$window.location.href = prefix + "/test?a=b&c=d"
|
$window.location.href = prefix + "/test?a=b&c=d"
|
||||||
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -140,7 +147,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("calls reject", function(done) {
|
o("calls reject", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/other": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/other": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onFail.callCount).equals(1)
|
o(onFail.callCount).equals(1)
|
||||||
|
|
@ -152,7 +159,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles out of order routes", function(done) {
|
o("handles out of order routes", function(done) {
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes({"/z/y/x": {data: 1}, "/:a...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/z/y/x": {data: 1}, "/:a...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -164,7 +171,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles reverse out of order routes", function(done) {
|
o("handles reverse out of order routes", function(done) {
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes({"/:a...": {data: 2}, "/z/y/x": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/:a...": {data: 2}, "/z/y/x": {data: 1}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -180,7 +187,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
routes["/:a..."] = {data: 2}
|
routes["/:a..."] = {data: 2}
|
||||||
|
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes(routes, onRouteChange, onFail)
|
defineRoutes(routes)
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -196,7 +203,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
routes["/z/y/x"] = {data: 1}
|
routes["/z/y/x"] = {data: 1}
|
||||||
|
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes(routes, onRouteChange, onFail)
|
defineRoutes(routes)
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -211,7 +218,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
routes["/:a..."] = {data: 2}
|
routes["/:a..."] = {data: 2}
|
||||||
|
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes(routes, onRouteChange, onFail)
|
defineRoutes(routes)
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -226,7 +233,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
routes["/z/y/x"] = {data: 12}
|
routes["/z/y/x"] = {data: 12}
|
||||||
|
|
||||||
$window.location.href = prefix + "/z/y/x"
|
$window.location.href = prefix + "/z/y/x"
|
||||||
router.defineRoutes(routes, onRouteChange, onFail)
|
defineRoutes(routes)
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
@ -238,7 +245,7 @@ o.spec("Router.defineRoutes", function() {
|
||||||
|
|
||||||
o("handles non-ascii routes", function(done) {
|
o("handles non-ascii routes", function(done) {
|
||||||
$window.location.href = prefix + "/ö"
|
$window.location.href = prefix + "/ö"
|
||||||
router.defineRoutes({"/ö": "aaa"}, onRouteChange, onFail)
|
defineRoutes({"/ö": "aaa"})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
o(onRouteChange.callCount).equals(1)
|
o(onRouteChange.callCount).equals(1)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ o.spec("Router.getPath", function() {
|
||||||
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 $window, router, onRouteChange, onFail
|
var $window, router, onRouteChange, onFail
|
||||||
|
|
||||||
|
function defineRoutes(routes, defaultRoute) {
|
||||||
|
router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {})
|
||||||
|
}
|
||||||
|
|
||||||
o.beforeEach(function() {
|
o.beforeEach(function() {
|
||||||
$window = pushStateMock(env)
|
$window = pushStateMock(env)
|
||||||
router = new Router($window)
|
router = new Router($window)
|
||||||
|
|
@ -20,25 +24,25 @@ o.spec("Router.getPath", function() {
|
||||||
|
|
||||||
o("gets route", function() {
|
o("gets route", function() {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}})
|
||||||
|
|
||||||
o(router.getPath()).equals("/test")
|
o(router.getPath()).equals("/test")
|
||||||
})
|
})
|
||||||
o("gets route w/ params", function() {
|
o("gets route w/ params", function() {
|
||||||
$window.location.href = prefix + "/other/x/y/z?c=d#e=f"
|
$window.location.href = prefix + "/other/x/y/z?c=d#e=f"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
o(router.getPath()).equals("/other/x/y/z?c=d#e=f")
|
o(router.getPath()).equals("/other/x/y/z?c=d#e=f")
|
||||||
})
|
})
|
||||||
o("gets route w/ escaped unicode", function() {
|
o("gets route w/ escaped unicode", function() {
|
||||||
$window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6"
|
$window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
|
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
|
||||||
})
|
})
|
||||||
o("gets route w/ unicode", function() {
|
o("gets route w/ unicode", function() {
|
||||||
$window.location.href = prefix + "/ö?ö=ö#ö=ö"
|
$window.location.href = prefix + "/ö?ö=ö#ö=ö"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
|
o(router.getPath()).equals("/ö?ö=ö#ö=ö")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ o.spec("Router.setPath", function() {
|
||||||
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 $window, router, onRouteChange, onFail
|
var $window, router, onRouteChange, onFail
|
||||||
|
|
||||||
|
function defineRoutes(routes, defaultRoute) {
|
||||||
|
router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {})
|
||||||
|
}
|
||||||
|
|
||||||
o.beforeEach(function() {
|
o.beforeEach(function() {
|
||||||
$window = pushStateMock(env)
|
$window = pushStateMock(env)
|
||||||
router = new Router($window)
|
router = new Router($window)
|
||||||
|
|
@ -21,7 +25,7 @@ o.spec("Router.setPath", function() {
|
||||||
|
|
||||||
o("setPath calls onRouteChange asynchronously", function(done) {
|
o("setPath calls onRouteChange asynchronously", function(done) {
|
||||||
$window.location.href = prefix + "/a"
|
$window.location.href = prefix + "/a"
|
||||||
router.defineRoutes({"/a": {data: 1}, "/b": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/a": {data: 1}, "/b": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/b")
|
router.setPath("/b")
|
||||||
|
|
@ -35,7 +39,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("setPath calls onFail asynchronously", function(done) {
|
o("setPath calls onFail asynchronously", function(done) {
|
||||||
$window.location.href = prefix + "/a"
|
$window.location.href = prefix + "/a"
|
||||||
router.defineRoutes({"/a": {data: 1}, "/b": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/a": {data: 1}, "/b": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/c")
|
router.setPath("/c")
|
||||||
|
|
@ -49,7 +53,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("sets route via API", function(done) {
|
o("sets route via API", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other/x/y/z?c=d#e=f")
|
router.setPath("/other/x/y/z?c=d#e=f")
|
||||||
|
|
@ -61,7 +65,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("sets route w/ escaped unicode", function(done) {
|
o("sets route w/ escaped unicode", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6")
|
router.setPath("/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6")
|
||||||
|
|
@ -73,7 +77,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("sets route w/ unicode", function(done) {
|
o("sets route w/ unicode", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/ö?ö=ö#ö=ö")
|
router.setPath("/ö?ö=ö#ö=ö")
|
||||||
|
|
@ -90,7 +94,7 @@ o.spec("Router.setPath", function() {
|
||||||
router = new Router($window)
|
router = new Router($window)
|
||||||
router.prefix = prefix
|
router.prefix = prefix
|
||||||
|
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other/x/y/z?c=d#e=f")
|
router.setPath("/other/x/y/z?c=d#e=f")
|
||||||
|
|
@ -102,7 +106,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("sets route via pushState/onpopstate", function(done) {
|
o("sets route via pushState/onpopstate", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
$window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f")
|
$window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f")
|
||||||
|
|
@ -115,7 +119,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("sets parameterized route", function(done) {
|
o("sets parameterized route", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"})
|
router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"})
|
||||||
|
|
@ -127,7 +131,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("replace:true works", function(done) {
|
o("replace:true works", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other", null, {replace: true})
|
router.setPath("/other", null, {replace: true})
|
||||||
|
|
@ -140,7 +144,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("replace:false works", function(done) {
|
o("replace:false works", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other", null, {replace: false})
|
router.setPath("/other", null, {replace: false})
|
||||||
|
|
@ -155,7 +159,7 @@ o.spec("Router.setPath", function() {
|
||||||
})
|
})
|
||||||
o("state works", function(done) {
|
o("state works", function(done) {
|
||||||
$window.location.href = prefix + "/test"
|
$window.location.href = prefix + "/test"
|
||||||
router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail)
|
defineRoutes({"/test": {data: 1}, "/other": {data: 2}})
|
||||||
|
|
||||||
callAsync(function() {
|
callAsync(function() {
|
||||||
router.setPath("/other", null, {state: {a: 1}})
|
router.setPath("/other", null, {state: {a: 1}})
|
||||||
|
|
|
||||||
|
|
@ -224,8 +224,16 @@ module.exports = function(options) {
|
||||||
return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
|
return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
|
||||||
}
|
}
|
||||||
var activeElement
|
var activeElement
|
||||||
|
var delay = 16, last = 0
|
||||||
var $window = {
|
var $window = {
|
||||||
DOMParser: DOMParser,
|
DOMParser: DOMParser,
|
||||||
|
requestAnimationFrame: function(callback) {
|
||||||
|
var elapsed = Date.now() - last
|
||||||
|
return setTimeout(function() {
|
||||||
|
callback()
|
||||||
|
last = Date.now()
|
||||||
|
}, delay - elapsed)
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
createElement: function(tag) {
|
createElement: function(tag) {
|
||||||
var cssText = ""
|
var cssText = ""
|
||||||
|
|
|
||||||
|
|
@ -187,5 +187,13 @@ module.exports = function(options) {
|
||||||
$window.onhashchange = null,
|
$window.onhashchange = null,
|
||||||
$window.onunload = null
|
$window.onunload = null
|
||||||
|
|
||||||
|
$window.addEventListener = function (name, handler) {
|
||||||
|
$window["on" + name] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
$window.removeEventListener = function (name, handler) {
|
||||||
|
$window["on" + name] = handler
|
||||||
|
}
|
||||||
|
|
||||||
return $window
|
return $window
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,16 @@ var browserMock = require("../test-utils/browserMock")
|
||||||
var components = require("../test-utils/components")
|
var components = require("../test-utils/components")
|
||||||
|
|
||||||
o.spec("api", function() {
|
o.spec("api", function() {
|
||||||
var m
|
|
||||||
var FRAME_BUDGET = Math.floor(1000 / 60)
|
var FRAME_BUDGET = Math.floor(1000 / 60)
|
||||||
o.beforeEach(function() {
|
var mock = browserMock(), root
|
||||||
var mock = browserMock()
|
if (typeof global !== "undefined") {
|
||||||
if (typeof global !== "undefined") global.window = mock
|
global.window = mock
|
||||||
m = require("../mithril") // eslint-disable-line global-require
|
global.requestAnimationFrame = mock.requestAnimationFrame
|
||||||
|
}
|
||||||
|
var m = require("..") // eslint-disable-line global-require
|
||||||
|
|
||||||
|
o.afterEach(function() {
|
||||||
|
if (root) m.mount(root, null)
|
||||||
})
|
})
|
||||||
|
|
||||||
o.spec("m", function() {
|
o.spec("m", function() {
|
||||||
|
|
@ -71,7 +75,7 @@ o.spec("api", function() {
|
||||||
})
|
})
|
||||||
o.spec("m.render", function() {
|
o.spec("m.render", function() {
|
||||||
o("works", function() {
|
o("works", function() {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.render(root, m("div"))
|
m.render(root, m("div"))
|
||||||
|
|
||||||
o(root.childNodes.length).equals(1)
|
o(root.childNodes.length).equals(1)
|
||||||
|
|
@ -84,7 +88,7 @@ o.spec("api", function() {
|
||||||
|
|
||||||
o.spec("m.mount", function() {
|
o.spec("m.mount", function() {
|
||||||
o("works", function() {
|
o("works", function() {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.mount(root, createComponent({view: function() {return m("div")}}))
|
m.mount(root, createComponent({view: function() {return m("div")}}))
|
||||||
|
|
||||||
o(root.childNodes.length).equals(1)
|
o(root.childNodes.length).equals(1)
|
||||||
|
|
@ -93,7 +97,7 @@ o.spec("api", function() {
|
||||||
})
|
})
|
||||||
o.spec("m.route", function() {
|
o.spec("m.route", function() {
|
||||||
o("works", function(done) {
|
o("works", function(done) {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.route(root, "/a", {
|
m.route(root, "/a", {
|
||||||
"/a": createComponent({view: function() {return m("div")}})
|
"/a": createComponent({view: function() {return m("div")}})
|
||||||
})
|
})
|
||||||
|
|
@ -106,7 +110,7 @@ o.spec("api", function() {
|
||||||
}, FRAME_BUDGET)
|
}, FRAME_BUDGET)
|
||||||
})
|
})
|
||||||
o("m.route.prefix", function(done) {
|
o("m.route.prefix", function(done) {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.route.prefix("#")
|
m.route.prefix("#")
|
||||||
m.route(root, "/a", {
|
m.route(root, "/a", {
|
||||||
"/a": createComponent({view: function() {return m("div")}})
|
"/a": createComponent({view: function() {return m("div")}})
|
||||||
|
|
@ -120,7 +124,7 @@ o.spec("api", function() {
|
||||||
}, FRAME_BUDGET)
|
}, FRAME_BUDGET)
|
||||||
})
|
})
|
||||||
o("m.route.get", function(done) {
|
o("m.route.get", function(done) {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.route(root, "/a", {
|
m.route(root, "/a", {
|
||||||
"/a": createComponent({view: function() {return m("div")}})
|
"/a": createComponent({view: function() {return m("div")}})
|
||||||
})
|
})
|
||||||
|
|
@ -133,7 +137,7 @@ o.spec("api", function() {
|
||||||
})
|
})
|
||||||
o("m.route.set", function(done, timeout) {
|
o("m.route.set", function(done, timeout) {
|
||||||
timeout(100)
|
timeout(100)
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.route(root, "/a", {
|
m.route(root, "/a", {
|
||||||
"/:id": createComponent({view: function() {return m("div")}})
|
"/:id": createComponent({view: function() {return m("div")}})
|
||||||
})
|
})
|
||||||
|
|
@ -151,7 +155,7 @@ o.spec("api", function() {
|
||||||
o.spec("m.redraw", function() {
|
o.spec("m.redraw", function() {
|
||||||
o("works", function(done) {
|
o("works", function(done) {
|
||||||
var count = 0
|
var count = 0
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
m.mount(root, createComponent({view: function() {count++}}))
|
m.mount(root, createComponent({view: function() {count++}}))
|
||||||
o(count).equals(1)
|
o(count).equals(1)
|
||||||
m.redraw()
|
m.redraw()
|
||||||
|
|
@ -164,7 +168,7 @@ o.spec("api", function() {
|
||||||
}, FRAME_BUDGET)
|
}, FRAME_BUDGET)
|
||||||
})
|
})
|
||||||
o("sync", function() {
|
o("sync", function() {
|
||||||
var root = window.document.createElement("div")
|
root = window.document.createElement("div")
|
||||||
var view = o.spy()
|
var view = o.spy()
|
||||||
m.mount(root, createComponent({view: view}))
|
m.mount(root, createComponent({view: view}))
|
||||||
o(view.callCount).equals(1)
|
o(view.callCount).equals(1)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue