From 7ab7ff5128c9c9037802170761e248a758016b51 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sat, 6 Jul 2019 14:54:11 -0400 Subject: [PATCH 1/7] 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. --- api/router.js | 178 ++++++++++++++----- api/tests/index.html | 2 +- api/tests/test-router.js | 162 ++++++++++++++++- api/tests/test-routerGetSet.js | 281 ++++++++++++++++++++++++++++++ route.js | 3 +- router/router.js | 112 ------------ router/tests/index.html | 26 --- router/tests/test-defineRoutes.js | 259 --------------------------- router/tests/test-getPath.js | 52 ------ router/tests/test-setPath.js | 175 ------------------- 10 files changed, 583 insertions(+), 667 deletions(-) create mode 100644 api/tests/test-routerGetSet.js delete mode 100644 router/router.js delete mode 100644 router/tests/index.html delete mode 100644 router/tests/test-defineRoutes.js delete mode 100644 router/tests/test-getPath.js delete mode 100644 router/tests/test-setPath.js diff --git a/api/router.js b/api/router.js index a388b40cf..166467fca 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, redrawService, mount) { + 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) redrawService.redraw() + else { + state = 2 + redrawService.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 mount(root, { + onbeforeupdate: function() { + state = state ? 2 : 1 + return !(!state || sentinel === currentResolver) + }, + oncreate: resolveRoute, + onremove: onremove, + view: function() { + if (!state || sentinel === currentResolver) return + var vnode = Vnode(component, attrs.key, attrs) + if (currentResolver) vnode = currentResolver.render(vnode) + // Wrap in a fragment to preserve existing key semantics + 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..976f74124 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -24,13 +24,13 @@ - + diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 1f7fc1d9a..5aed53bde 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -7,6 +7,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") +var apiMount = require("../../api/mount") var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -24,7 +25,7 @@ o.spec("route", function() { root = $window.document.body redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService) + route = apiRouter($window, redrawService, apiMount(redrawService)) route.prefix(prefix) }) @@ -55,6 +56,165 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) + o("resolves to route w/ escaped unicode", function() { + $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" + route(root, "/ö", { + "/ö" : { + view: function() { + return m("div") + } + } + }) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("resolves to route w/ unicode", function() { + $window.location.href = prefix + "/ö?ö=ö" + route(root, "/ö", { + "/ö" : { + view: function() { + return JSON.stringify(route.param()) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals('{"ö":"ö"} /ö?ö=ö') + }) + + o("handles parameterized route", function() { + $window.location.href = prefix + "/test/x" + route(root, "/test/:a", { + "/test/:a" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x"} {"a":"x"} /test/x' + ) + }) + + o("handles multi-parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a/:b", { + "/test/:a/:b" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x","b":"y"} {"a":"x","b":"y"} /test/x/y' + ) + }) + + o("handles rest parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + route(root, "/test/:a...", { + "/test/:a..." : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"x/y"} {"a":"x/y"} /test/x/y' + ) + }) + + o("handles route with search", function() { + $window.location.href = prefix + "/test?a=b&c=d" + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals( + '{"a":"b","c":"d"} {"a":"b","c":"d"} /test?a=b&c=d' + ) + }) + + o("redirects to default route if no match", function(done) { + $window.location.href = prefix + "/test" + route(root, "/other", { + "/other": { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + callAsync(function() { + o(root.firstChild.nodeValue).equals("{} {} /other") + done() + }) + }) + + o("handles out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/z/y/x": { + view: function() { return "1" }, + }, + "/:a...": { + view: function() { return "2" }, + }, + }) + + o(root.firstChild.nodeValue).equals("1") + }) + + o("handles reverse out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + + route(root, "/z/y/x", { + "/:a...": { + view: function() { return "2" }, + }, + "/z/y/x": { + view: function() { return "1" }, + }, + }) + + o(root.firstChild.nodeValue).equals("2") + }) + + o("resolves to route on fallback mode", function() { + $window.location.href = "file://" + prefix + "/test" + + route(root, "/test", { + "/test" : { + view: function(vnode) { + return JSON.stringify(route.param()) + " " + + JSON.stringify(vnode.attrs) + " " + + route.get() + } + } + }) + + o(root.firstChild.nodeValue).equals("{} {} /test") + }) + o("routed mount points only redraw asynchronously (POJO component)", function() { var view = o.spy() diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js new file mode 100644 index 000000000..57ec0c406 --- /dev/null +++ b/api/tests/test-routerGetSet.js @@ -0,0 +1,281 @@ +"use strict" + +var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") +var browserMock = require("../../test-utils/browserMock") +var throttleMocker = require("../../test-utils/throttleMock") + +var callAsync = require("../../test-utils/callAsync") +var apiMount = require("../../api/mount") +var apiRedraw = require("../../api/redraw") +var apiRouter = require("../../api/router") + +o.spec("route.get/route.set", function() { + void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { + void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { + o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { + var $window, root, redrawService, route, throttleMock + + o.beforeEach(function() { + $window = browserMock(env) + throttleMock = throttleMocker() + + root = $window.document.body + + redrawService = apiRedraw($window, throttleMock.throttle) + route = apiRouter($window, redrawService, apiMount(redrawService)) + route.prefix(prefix) + }) + + o.afterEach(function() { + o(throttleMock.queueLength()).equals(0) + }) + + o("gets route", function() { + $window.location.href = prefix + "/test" + route(root, "/test", {"/test": {view: function() {}}}) + + o(route.get()).equals("/test") + }) + + o("gets route w/ params", function() { + $window.location.href = prefix + "/other/x/y/z?c=d#e=f" + + route(root, "/other/x/y/z?c=d#e=f", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/other/x/y/z?c=d#e=f") + }) + + o("gets route w/ escaped unicode", function() { + $window.location.href = prefix + encodeURI("/ö/é/å?ö=ö#ö=ö") + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("gets route w/ unicode", function() { + $window.location.href = prefix + "/ö/é/å?ö=ö#ö=ö" + + route(root, "/ö/é/å?ö=ö#ö=ö", { + "/test": {view: function() {}}, + "/ö/:a/:b...": {view: function() {}}, + }) + + o(route.get()).equals("/ö/é/å?ö=ö#ö=ö") + }) + + o("sets path asynchronously", function(done) { + $window.location.href = prefix + "/a" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + route.set("/b") + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(0) + callAsync(function() { + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + + o("sets fallback asynchronously", function(done) { + $window.location.href = prefix + "/b" + var spy1 = o.spy() + var spy2 = o.spy() + + route(root, "/a", { + "/a": {view: spy1}, + "/b": {view: spy2}, + }) + + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + route.set("/c") + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(1) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/b") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/a") + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + done() + }) + }) + }) + + o("exposes new route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("exposes new escaped unicode route asynchronously", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set(encodeURI("/ö?ö=ö#ö=ö")) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new unescaped unicode route asynchronously", function(done) { + $window.location.href = "file://" + prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/ö": {view: function() {}}, + }) + + route.set("/ö?ö=ö#ö=ö") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/ö?ö=ö#ö=ö") + throttleMock.fire() + done() + }) + }) + + o("exposes new route asynchronously on fallback mode", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/x/y/z?c=d#e=f") + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + done() + }) + }) + + o("sets route via pushState/onpopstate", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + callAsync(function() { + $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") + $window.onpopstate() + + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y/z?c=d#e=f") + throttleMock.fire() + + done() + }) + }) + }) + + o("sets parameterized route", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other/:a/:b...": {view: function() {}}, + }) + + route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) + callAsync(function() { + // Yep, before even the throttle mechanism takes hold. + o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") + throttleMock.fire() + done() + }) + }) + + o("replace:true works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: true}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") + done() + }) + }) + + o("replace:false works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {replace: false}) + + callAsync(function() { + throttleMock.fire() + $window.history.back() + var slash = prefix[0] === "/" ? "" : "/" + o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") + done() + }) + }) + + o("state works", function(done) { + $window.location.href = prefix + "/test" + route(root, "/test", { + "/test": {view: function() {}}, + "/other": {view: function() {}}, + }) + + route.set("/other", null, {state: {a: 1}}) + callAsync(function() { + throttleMock.fire() + o($window.history.state).deepEquals({a: 1}) + done() + }) + }) + }) + }) + }) +}) diff --git a/route.js b/route.js index 4e8295276..a29cd9f67 100644 --- a/route.js +++ b/route.js @@ -1,5 +1,6 @@ "use strict" var redrawService = require("./redraw") +var mount = require("./mount") -module.exports = require("./api/router")(window, redrawService) +module.exports = require("./api/router")(window, redrawService, mount) diff --git a/router/router.js b/router/router.js deleted file mode 100644 index 37fc29b5b..000000000 --- a/router/router.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict" - -var buildPathname = require("../pathname/build") -var parsePathname = require("../pathname/parse") -var compileTemplate = require("../pathname/compileTemplate") -var assign = require("../pathname/assign") - -module.exports = function($window) { - var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout - var supportsPushState = typeof $window.history.pushState === "function" - var fireAsync - - return { - prefix: "#!", - - getPath: function() { - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash - if (this.prefix[0] !== "#") { - prefix = $window.location.search + prefix - if (this.prefix[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. - return prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) - .slice(this.prefix.length) - }, - - setPath: function(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, this.prefix + path) - else $window.history.pushState(state, title, this.prefix + path) - } - else { - $window.location.href = this.prefix + path - } - }, - - defineRoutes: function(routes, resolve, reject, defaultRoute, subscribe) { - var self = this - 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 unsubscribe, 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 resolveRoute() { - var path = self.getPath() - var data = parsePathname(path) - - assign(data.params, $window.history.state) - - for (var i = 0; i < compiled.length; i++) { - if (compiled[i].check(data)) { - resolve(compiled[i].component, data.params, path, compiled[i].route) - return - } - } - - reject(path, data.params) - } - - if (supportsPushState) { - unsubscribe = function() { - $window.removeEventListener("popstate", fireAsync, false) - } - $window.addEventListener("popstate", fireAsync = function() { - if (asyncId) return - asyncId = callAsync(function() { - asyncId = null - resolveRoute() - }) - }, false) - } else if (this.prefix[0] === "#") { - unsubscribe = function() { - $window.removeEventListener("hashchange", resolveRoute, false) - } - $window.addEventListener("hashchange", resolveRoute, false) - } - - subscribe(unsubscribe) - resolveRoute() - }, - } -} diff --git a/router/tests/index.html b/router/tests/index.html deleted file mode 100644 index 1f186449c..000000000 --- a/router/tests/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/router/tests/test-defineRoutes.js b/router/tests/test-defineRoutes.js deleted file mode 100644 index 39b85366b..000000000 --- a/router/tests/test-defineRoutes.js +++ /dev/null @@ -1,259 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.defineRoutes", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "?#", "##"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("calls onRouteChange on init", function(done) { - $window.location.href = prefix + "/a" - var subscribe = o.spy() - - router.defineRoutes({"/a": {data: 1}}, onRouteChange, onFail, null, subscribe) - o(subscribe.callCount).equals(1) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - - done() - }) - }) - - o("resolves to route", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route w/ escaped unicode", function(done) { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6" - defineRoutes({"/ö": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö", "/ö"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route w/ unicode", function(done) { - $window.location.href = prefix + "/ö?ö=ö" - defineRoutes({"/ö": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {"ö": "ö"}, "/ö?ö=ö", "/ö"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("resolves to route on fallback mode", function(done) { - $window.location.href = "file://" + prefix + "/test" - - router = new Router($window) - router.prefix = prefix - - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles parameterized route", function(done) { - $window.location.href = prefix + "/test/x" - defineRoutes({"/test/:a": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x"}, "/test/x", "/test/:a"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles multi-parameterized route", function(done) { - $window.location.href = prefix + "/test/x/y" - defineRoutes({"/test/:a/:b": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x", b: "y"}, "/test/x/y", "/test/:a/:b"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles rest parameterized route", function(done) { - $window.location.href = prefix + "/test/x/y" - defineRoutes({"/test/:a...": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "x/y"}, "/test/x/y", "/test/:a..."]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("handles route with search", function(done) { - $window.location.href = prefix + "/test?a=b&c=d" - defineRoutes({"/test": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b&c=d", "/test"]) - o(onFail.callCount).equals(0) - - done() - }) - }) - - o("calls reject", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/other": {data: 1}}) - - callAsync(function() { - o(onFail.callCount).equals(1) - o(onFail.args).deepEquals(["/test", {}]) - - done() - }) - }) - - o("handles out of order routes", function(done) { - $window.location.href = prefix + "/z/y/x" - defineRoutes({"/z/y/x": {data: 1}, "/:a...": {data: 2}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reverse out of order routes", function(done) { - $window.location.href = prefix + "/z/y/x" - defineRoutes({"/:a...": {data: 2}, "/z/y/x": {data: 1}}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles dynamically added out of order routes", function(done) { - var routes = {} - routes["/z/y/x"] = {data: 1} - routes["/:a..."] = {data: 2} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reversed dynamically added out of order routes", function(done) { - var routes = {} - routes["/:a..."] = {data: 2} - routes["/z/y/x"] = {data: 1} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles mixed out of order routes", function(done) { - var routes = {"/z/y/x": {data: 1}} - routes["/:a..."] = {data: 2} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) - - done() - }) - }) - - o("handles reverse mixed out of order routes", function(done) { - var routes = {"/:a...": {data: 2}} - routes["/z/y/x"] = {data: 12} - - $window.location.href = prefix + "/z/y/x" - defineRoutes(routes) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) - - done() - }) - }) - - o("handles non-ascii routes", function(done) { - $window.location.href = prefix + "/ö" - defineRoutes({"/ö": "aaa"}) - - callAsync(function() { - o(onRouteChange.callCount).equals(1) - - done() - }) - }) - }) - }) - }) -}) diff --git a/router/tests/test-getPath.js b/router/tests/test-getPath.js deleted file mode 100644 index f79e133fb..000000000 --- a/router/tests/test-getPath.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.getPath", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo", "?#", "##"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("gets route", function() { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}}) - - o(router.getPath()).equals("/test") - }) - o("gets route w/ params", function() { - $window.location.href = prefix + "/other/x/y/z?c=d#e=f" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - }) - o("gets route w/ escaped unicode", function() { - $window.location.href = prefix + "/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - }) - o("gets route w/ unicode", function() { - $window.location.href = prefix + "/ö?ö=ö#ö=ö" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - }) - }) - }) - }) -}) diff --git a/router/tests/test-setPath.js b/router/tests/test-setPath.js deleted file mode 100644 index 8447c5126..000000000 --- a/router/tests/test-setPath.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var pushStateMock = require("../../test-utils/pushStateMock") -var Router = require("../../router/router") - -o.spec("Router.setPath", function() { - void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { - o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, router, onRouteChange, onFail - - function defineRoutes(routes, defaultRoute) { - router.defineRoutes(routes, onRouteChange, onFail, defaultRoute, function() {}) - } - - o.beforeEach(function() { - $window = pushStateMock(env) - router = new Router($window) - router.prefix = prefix - onRouteChange = o.spy() - onFail = o.spy() - }) - - o("setPath calls onRouteChange asynchronously", function(done) { - $window.location.href = prefix + "/a" - defineRoutes({"/a": {data: 1}, "/b": {data: 2}}) - - callAsync(function() { - router.setPath("/b") - - o(onRouteChange.callCount).equals(1) - callAsync(function() { - o(onRouteChange.callCount).equals(2) - done() - }) - }) - }) - o("setPath calls onFail asynchronously", function(done) { - $window.location.href = prefix + "/a" - defineRoutes({"/a": {data: 1}, "/b": {data: 2}}) - - callAsync(function() { - router.setPath("/c") - - o(onFail.callCount).equals(0) - callAsync(function() { - o(onFail.callCount).equals(1) - done() - }) - }) - }) - o("sets route via API", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/x/y/z?c=d#e=f") - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets route w/ escaped unicode", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/%C3%B6?%C3%B6=%C3%B6#%C3%B6=%C3%B6") - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - - done() - }) - }) - o("sets route w/ unicode", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/ö/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/ö?ö=ö#ö=ö") - - o(router.getPath()).equals("/ö?ö=ö#ö=ö") - - done() - }) - }) - - o("sets route on fallback mode", function(done) { - $window.location.href = "file://" + prefix + "/test" - - router = new Router($window) - router.prefix = prefix - - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/x/y/z?c=d#e=f") - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets route via pushState/onpopstate", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") - $window.onpopstate() - - o(router.getPath()).equals("/other/x/y/z?c=d#e=f") - - done() - }) - }) - o("sets parameterized route", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}) - - callAsync(function() { - router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) - - o(router.getPath()).equals("/other/x/y%2Fz?c=d&e=f") - - done() - }) - }) - o("replace:true works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {replace: true}) - $window.history.back() - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + "/") - - done() - }) - }) - o("replace:false works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {replace: false}) - $window.history.back() - - var slash = prefix[0] === "/" ? "" : "/" - - o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") - - done() - }) - }) - o("state works", function(done) { - $window.location.href = prefix + "/test" - defineRoutes({"/test": {data: 1}, "/other": {data: 2}}) - - callAsync(function() { - router.setPath("/other", null, {state: {a: 1}}) - - o($window.history.state).deepEquals({a: 1}) - - done() - }) - }) - }) - }) - }) -}) From c191910770e0440398e8addcc66fad38c4ba3fe0 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sat, 6 Jul 2019 16:14:20 -0400 Subject: [PATCH 2/7] 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. --- api/mount-redraw.js | 52 ++++ api/mount.js | 15 - api/redraw.js | 58 ---- api/router.js | 8 +- api/tests/index.html | 6 +- api/tests/test-mount.js | 274 ----------------- api/tests/test-mountRedraw.js | 413 ++++++++++++++++++++++++++ api/tests/test-redraw.js | 195 ------------ api/tests/test-router.js | 34 +-- api/tests/test-routerGetSet.js | 9 +- docs/index.md | 2 +- docs/route.md | 2 +- index.js | 8 +- mount-redraw.js | 3 + mount.js | 4 +- redraw.js | 2 +- render/render.js | 2 +- route.js | 5 +- test-utils/tests/test-throttleMock.js | 63 +--- test-utils/throttleMock.js | 13 +- 20 files changed, 512 insertions(+), 656 deletions(-) create mode 100644 api/mount-redraw.js delete mode 100644 api/mount.js delete mode 100644 api/redraw.js delete mode 100644 api/tests/test-mount.js create mode 100644 api/tests/test-mountRedraw.js delete mode 100644 api/tests/test-redraw.js create mode 100644 mount-redraw.js diff --git a/api/mount-redraw.js b/api/mount-redraw.js new file mode 100644 index 000000000..0cbecb1f5 --- /dev/null +++ b/api/mount-redraw.js @@ -0,0 +1,52 @@ +"use strict" + +var Vnode = require("../render/vnode") +var coreRenderer = require("../render/render") + +module.exports = function($window, schedule, console) { + var renderService = coreRenderer($window) + 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 { renderService.render(subscriptions[i], Vnode(subscriptions[i + 1])) } + catch (e) { console.error(e) } + } + rendering = false + } + function redraw() { + if (!pending) { + pending = true + schedule(function() { + pending = false + sync() + }) + } + } + + redraw.sync = sync + renderService.setRedraw(redraw) + + 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) + renderService.render(root, []) + } + + if (component != null) { + subscriptions.push(root, component) + renderService.render(root, Vnode(component)) + } + } + + 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 166467fca..cc0795929 100644 --- a/api/router.js +++ b/api/router.js @@ -10,7 +10,7 @@ var assign = require("../pathname/assign") var sentinel = {} -module.exports = function($window, redrawService, mount) { +module.exports = function($window, mountRedraw) { var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout var supportsPushState = typeof $window.history.pushState === "function" var routePrefix = "#!" @@ -91,10 +91,10 @@ module.exports = function($window, redrawService, mount) { 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) redrawService.redraw() + if (state === 2) mountRedraw.redraw() else { state = 2 - redrawService.redraw.sync() + mountRedraw.redraw.sync() } } if (payload.view || typeof payload === "function") update({}, payload) @@ -135,7 +135,7 @@ module.exports = function($window, redrawService, mount) { $window.addEventListener("hashchange", resolveRoute, false) } - return mount(root, { + return mountRedraw.mount(root, { onbeforeupdate: function() { state = state ? 2 : 1 return !(!state || sentinel === currentResolver) diff --git a/api/tests/index.html b/api/tests/index.html index 976f74124..e7d28bdbc 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -24,11 +24,9 @@ - - + - - + diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js deleted file mode 100644 index 7385e43d5..000000000 --- a/api/tests/test-mount.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var m = require("../../render/hyperscript") -var apiRedraw = require("../../api/redraw") -var apiMounter = require("../../api/mount") - -o.spec("mount", function() { - var $window, root, redrawService, mount, render, throttleMock - - o.beforeEach(function() { - $window = domMock() - throttleMock = throttleMocker() - - root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - mount = apiMounter(redrawService) - render = redrawService.render - }) - - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - }) - - o("throws on invalid component", function() { - var threw = false - try { - mount(root, {}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - mount(null, createComponent({view: function() {}})) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("renders into `root` synchronoulsy", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("mounting null unmounts", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("Mounting a second root doesn't cause the first one to redraw", function() { - var view = o.spy(function() { - return m("div") - }) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : view - })) - - o(root.firstChild.nodeName).equals("DIV") - o(view.callCount).equals(1) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div") - } - })) - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(1) - }) - - o("redraws on events", function() { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view : function() { - return m("div", { - oninit : oninit, - onupdate : onupdate, - onclick : onclick, - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("redraws several mount points on events", function() { - var onupdate0 = o.spy() - var oninit0 = o.spy() - var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() - var onclick1 = o.spy() - - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : function() { - return m("div", { - oninit : oninit0, - onupdate : onupdate0, - onclick : onclick0, - }) - } - })) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div", { - oninit : oninit1, - onupdate : onupdate1, - onclick : onclick1, - }) - } - })) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root.childNodes[0].firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root.childNodes[0].firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root.childNodes[1].firstChild.dispatchEvent(e) - - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) - }) - - o("event handlers can skip redraw", function() { - var onupdate = o.spy(function(){ - throw new Error("This shouldn't have been called") - }) - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false - } - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - throttleMock.fire() - - o(onupdate.callCount).equals(0) - }) - - o("redraws when the render function is run", function() { - var onupdate = o.spy() - var oninit = o.spy() - - mount(root, createComponent({ - view : function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - })) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - redrawService.redraw() - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("throttles", function() { - var i = 0 - mount(root, createComponent({view: function() {i++}})) - var before = i - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - var after = i - - throttleMock.fire() - - o(before).equals(1) // mounts synchronously - o(after).equals(1) // throttles rest - o(i).equals(2) - }) - }) - }) -}) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js new file mode 100644 index 000000000..7cb4432f6 --- /dev/null +++ b/api/tests/test-mountRedraw.js @@ -0,0 +1,413 @@ +"use strict" + +var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") +var domMock = require("../../test-utils/domMock") +var throttleMocker = require("../../test-utils/throttleMock") +var mountRedraw = require("../../api/mount-redraw") +var h = require("../../render/hyperscript") + +// Because Node doesn't have this. +if (typeof requestAnimationFrame !== "function") { + global.requestAnimationFrame = (function(delay, last) { + return function(callback) { + var elapsed = Date.now() - last + return setTimeout(function() { + callback() + last = Date.now() + }, delay - elapsed) + } + })(16, 0) +} + +o.spec("mount/redraw", function() { + var root, m, throttleMock, consoleMock, $document, errors + o.beforeEach(function() { + var $window = domMock() + consoleMock = {error: o.spy()} + throttleMock = throttleMocker() + root = $window.document.body + m = mountRedraw($window, throttleMock.schedule, consoleMock) + $document = $window.document + errors = [] + }) + + o.afterEach(function() { + o(consoleMock.error.calls.map(function(c) { + return c.args[0] + })).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) + }) + + o("shouldn't error if there are no renderers", function() { + m.redraw() + throttleMock.fire() + }) + + o("schedules correctly", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.redraw() + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("should run a single renderer entry", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + + o(spy.callCount).equals(1) + + m.redraw() + m.redraw() + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("should run all renderer entries", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + m.mount(el1, {view: spy1}) + m.mount(el2, {view: spy2}) + m.mount(el3, {view: spy3}) + + m.redraw() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + m.redraw() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + throttleMock.fire() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + }) + + o("should stop running after mount null", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root, null) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should stop running after mount undefined", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root, undefined) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should stop running after mount no arg", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should invoke remove callback on unmount", function() { + var spy = o.spy() + var onremove = o.spy() + + m.mount(root, {view: spy, onremove: onremove}) + o(spy.callCount).equals(1) + m.mount(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() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.redraw() + m.mount(root) + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("does nothing on invalid unmount", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + + m.mount(null) + m.redraw() + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("redraw.sync() redraws all roots synchronously", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + m.mount(el1, {view: spy1}) + m.mount(el2, {view: spy2}) + m.mount(el3, {view: spy3}) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + m.redraw.sync() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + + m.redraw.sync() + + o(spy1.callCount).equals(3) + o(spy2.callCount).equals(3) + o(spy3.callCount).equals(3) + }) + + + o("throws on invalid component", function() { + o(function() { m.mount(root, {}) }).throws(TypeError) + }) + + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("throws on invalid `root` DOM node", function() { + o(function() { + m.mount(null, createComponent({view: function() {}})) + }).throws(TypeError) + }) + + o("renders into `root` synchronously", function() { + m.mount(root, createComponent({ + view: function() { + return h("div") + } + })) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("mounting null unmounts", function() { + m.mount(root, createComponent({ + view: function() { + return h("div") + } + })) + + m.mount(root, null) + + o(root.childNodes.length).equals(0) + }) + + o("Mounting a second root doesn't cause the first one to redraw", function() { + var root1 = $document.createElement("div") + var root2 = $document.createElement("div") + var view = o.spy() + + m.mount(root1, createComponent({view: view})) + o(view.callCount).equals(1) + + m.mount(root2, createComponent({view: function() {}})) + + o(view.callCount).equals(1) + + throttleMock.fire() + o(view.callCount).equals(1) + }) + + o("redraws on events", function() { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate, + onclick: onclick, + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + throttleMock.fire() + + o(onupdate.callCount).equals(1) + }) + + o("redraws several mount points on events", function() { + var onupdate0 = o.spy() + var oninit0 = o.spy() + var onclick0 = o.spy() + var onupdate1 = o.spy() + var oninit1 = o.spy() + var onclick1 = o.spy() + + var root1 = $document.createElement("div") + var root2 = $document.createElement("div") + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root1, createComponent({ + view: function() { + return h("div", { + oninit: oninit0, + onupdate: onupdate0, + onclick: onclick0, + }) + } + })) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + m.mount(root2, createComponent({ + view: function() { + return h("div", { + oninit: oninit1, + onupdate: onupdate1, + onclick: onclick1, + }) + } + })) + + o(oninit1.callCount).equals(1) + o(onupdate1.callCount).equals(0) + + root1.firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root1.firstChild) + + throttleMock.fire() + + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) + + root2.firstChild.dispatchEvent(e) + + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root2.firstChild) + + throttleMock.fire() + + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) + }) + + o("event handlers can skip redraw", function() { + var onupdate = o.spy(function(){ + throw new Error("This shouldn't have been called") + }) + var oninit = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate, + onclick: function(e) { + e.redraw = false + } + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + throttleMock.fire() + + o(onupdate.callCount).equals(0) + }) + + o("redraws when the render function is run", function() { + var onupdate = o.spy() + var oninit = o.spy() + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate + }) + } + })) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + m.redraw() + + throttleMock.fire() + + o(onupdate.callCount).equals(1) + }) + }) + }) +}) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js deleted file mode 100644 index a04dbe847..000000000 --- a/api/tests/test-redraw.js +++ /dev/null @@ -1,195 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") -var apiRedraw = require("../../api/redraw") - -// Because Node doesn't have this. -if (typeof requestAnimationFrame !== "function") { - global.requestAnimationFrame = (function (delay, last) { - return function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - } - })(16, 0) -} - -o.spec("redrawService", function() { - var root, redrawService, $document - o.beforeEach(function() { - var $window = domMock() - root = $window.document.body - redrawService = apiRedraw($window) - $document = $window.document - }) - - o("shouldn't error if there are no renderers", function() { - redrawService.redraw() - }) - - o("honours throttleMock", function() { - var throttleMock = throttleMocker() - redrawService = apiRedraw(domMock(), throttleMock.throttle) - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - - o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(2) - }) - - o("should run a single renderer entry", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - o("should run all renderer entries", function(done) { - var el1 = $document.createElement("div") - var el2 = $document.createElement("div") - var el3 = $document.createElement("div") - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - redrawService.redraw() - - o(spy1.callCount).equals(1) - o(spy2.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() - }, 20) - }) - - o("should stop running after unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - redrawService.unsubscribe(root) - - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("should invoke remove callback on unsubscribe", function() { - var spy = o.spy() - 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) - o(spy.callCount).equals(1) - - redrawService.redraw() - - redrawService.unsubscribe(root) - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("does nothing on invalid unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - - redrawService.unsubscribe(null) - redrawService.redraw() - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - o("redraw.sync() redraws all roots synchronously", function() { - var el1 = $document.createElement("div") - var el2 = $document.createElement("div") - var el3 = $document.createElement("div") - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - redrawService.redraw.sync() - - o(spy1.callCount).equals(2) - o(spy2.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) - }) -}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 5aed53bde..5ec418398 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -7,8 +7,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") -var apiMount = require("../../api/mount") -var apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -16,7 +15,7 @@ o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, redrawService, route, throttleMock + var $window, root, mountRedraw, route, throttleMock o.beforeEach(function() { $window = browserMock(env) @@ -24,8 +23,8 @@ o.spec("route", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService, apiMount(redrawService)) + mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) @@ -223,7 +222,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -243,7 +242,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -262,7 +261,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -284,8 +283,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") - // unsubscribe as if via `m.mount(root)` - redrawService.unsubscribe(root) + mountRedraw.mount(root) o(root.childNodes.length).equals(0) }) @@ -352,7 +350,7 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(onupdate.callCount).equals(1) @@ -821,7 +819,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -857,7 +855,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -1182,7 +1180,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(view.callCount).equals(2) @@ -1446,10 +1444,10 @@ o.spec("route", function() { }) var before = i - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() var after = i throttleMock.fire() diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 57ec0c406..d507b97b8 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -6,15 +6,14 @@ var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var callAsync = require("../../test-utils/callAsync") -var apiMount = require("../../api/mount") -var apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") o.spec("route.get/route.set", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, redrawService, route, throttleMock + var $window, root, mountRedraw, route, throttleMock o.beforeEach(function() { $window = browserMock(env) @@ -22,8 +21,8 @@ o.spec("route.get/route.set", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService, apiMount(redrawService)) + mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) diff --git a/docs/index.md b/docs/index.md index 8589f517f..4fbffd826 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ It's small (< 10kb gzip), fast and provides routing and XHR utilities out of the
Download size
- Mithril (9.6kb) + Mithril (9.4kb)
Vue + Vue-Router + Vuex + fetch (40kb)
diff --git a/docs/route.md b/docs/route.md index 787ab0a83..06caa4ceb 100644 --- a/docs/route.md +++ b/docs/route.md @@ -739,7 +739,7 @@ m.route(document.body, "/", { 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. +- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. `m.route` uses `m.mount` internally to hook everything up, so it's not magic. Here's an example with React: diff --git a/index.js b/index.js index 1f775a5dc..120ccb29a 100644 --- a/index.js +++ b/index.js @@ -7,14 +7,14 @@ m.trust = hyperscript.trust m.fragment = hyperscript.fragment var requestService = require("./request") -var redrawService = require("./redraw") +var mountRedraw = require("./mount-redraw") -requestService.setCompletionCallback(redrawService.redraw) +requestService.setCompletionCallback(mountRedraw.redraw) -m.mount = require("./mount") +m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render").render -m.redraw = redrawService.redraw +m.redraw = mountRedraw.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = require("./querystring/parse") diff --git a/mount-redraw.js b/mount-redraw.js new file mode 100644 index 000000000..bcdf52ef7 --- /dev/null +++ b/mount-redraw.js @@ -0,0 +1,3 @@ +"use strict" + +module.exports = require("./api/mount-redraw")(window, requestAnimationFrame, console) diff --git a/mount.js b/mount.js index 73fa1ce9d..6e169443f 100644 --- a/mount.js +++ b/mount.js @@ -1,5 +1,3 @@ "use strict" -var redrawService = require("./redraw") - -module.exports = require("./api/mount")(redrawService) +module.exports = require("./mount-redraw").mount diff --git a/redraw.js b/redraw.js index 314e5002a..af43394dc 100644 --- a/redraw.js +++ b/redraw.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./api/redraw")(window) +module.exports = require("./mount-redraw").redraw diff --git a/render/render.js b/render/render.js index 7a4611986..0876ae37a 100644 --- a/render/render.js +++ b/render/render.js @@ -867,7 +867,7 @@ module.exports = function($window) { } function render(dom, vnodes) { - if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") + if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = activeElement() var namespace = dom.namespaceURI diff --git a/route.js b/route.js index a29cd9f67..9cf121e23 100644 --- a/route.js +++ b/route.js @@ -1,6 +1,5 @@ "use strict" -var redrawService = require("./redraw") -var mount = require("./mount") +var mountRedraw = require("./mount-redraw") -module.exports = require("./api/router")(window, redrawService, mount) +module.exports = require("./api/router")(window, mountRedraw) diff --git a/test-utils/tests/test-throttleMock.js b/test-utils/tests/test-throttleMock.js index 699206236..fd00d2bd0 100644 --- a/test-utils/tests/test-throttleMock.js +++ b/test-utils/tests/test-throttleMock.js @@ -4,88 +4,35 @@ var o = require("../../ospec/ospec") var throttleMocker = require("../../test-utils/throttleMock") o.spec("throttleMock", function() { - o("works with one callback", function() { + o("schedules one callback", function() { var throttleMock = throttleMocker() var spy = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled = throttleMock.throttle(spy) - - o(throttleMock.queueLength()).equals(0) - o(spy.callCount).equals(0) - - throttled() - + throttleMock.schedule(spy) o(throttleMock.queueLength()).equals(1) o(spy.callCount).equals(0) - - throttled() - - o(throttleMock.queueLength()).equals(1) - o(spy.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(1) }) - o("works with two callbacks", function() { + o("schedules two callbacks", function() { var throttleMock = throttleMocker() var spy1 = o.spy() var spy2 = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled1 = throttleMock.throttle(spy1) - - o(throttleMock.queueLength()).equals(0) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled1() - + throttleMock.schedule(spy1) o(throttleMock.queueLength()).equals(1) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled1() - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - var throttled2 = throttleMock.throttle(spy2) - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled2() - + throttleMock.schedule(spy2) o(throttleMock.queueLength()).equals(2) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled2() - - o(throttleMock.queueLength()).equals(2) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) - - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) }) }) diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index 6cdb57103..21eb53be6 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -3,17 +3,8 @@ module.exports = function() { var queue = [] return { - throttle: function(fn) { - var pending = false - return function() { - if (!pending) { - queue.push(function(){ - pending = false - fn() - }) - pending = true - } - } + schedule: function(fn) { + queue.push(fn) }, fire: function() { var tasks = queue From 91d39f6e99f14626b347fc11dadb115ec804b507 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 16:37:45 -0400 Subject: [PATCH 3/7] Make `mithril/render` just return the `m.render` function directly. --- index.js | 2 +- render.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 120ccb29a..3756e6795 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ requestService.setCompletionCallback(mountRedraw.redraw) m.mount = mountRedraw.mount m.route = require("./route") -m.render = require("./render").render +m.render = require("./render") m.redraw = mountRedraw.redraw m.request = requestService.request m.jsonp = requestService.jsonp diff --git a/render.js b/render.js index a6f8ce8d2..0e372a0a5 100644 --- a/render.js +++ b/render.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./render/render")(window) +module.exports = require("./render/render")(window).render From 0eee76e7912c51ba0c439f7386d3b6a5729481a3 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 17:17:57 -0400 Subject: [PATCH 4/7] 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 --- api/mount-redraw.js | 10 +++++----- api/router.js | 6 +++--- api/tests/test-router.js | 6 ++---- docs/change-log.md | 4 ++++ docs/hyperscript.md | 2 +- docs/keys.md | 2 +- docs/route.md | 4 +++- performance/test-perf.js | 2 +- render.js | 2 +- render/render.js | 9 ++------- render/tests/test-attributes.js | 12 ++++++------ render/tests/test-component.js | 2 +- render/tests/test-createElement.js | 2 +- render/tests/test-createFragment.js | 2 +- render/tests/test-createHTML.js | 2 +- render/tests/test-createNodes.js | 2 +- render/tests/test-createText.js | 2 +- render/tests/test-event.js | 5 ++--- render/tests/test-input.js | 2 +- render/tests/test-normalizeComponentChildren.js | 2 +- render/tests/test-onbeforeremove.js | 2 +- render/tests/test-onbeforeupdate.js | 2 +- render/tests/test-oncreate.js | 2 +- render/tests/test-oninit.js | 2 +- render/tests/test-onremove.js | 2 +- render/tests/test-onupdate.js | 2 +- render/tests/test-render-hyperscript-integration.js | 2 +- render/tests/test-render.js | 2 +- render/tests/test-textContent.js | 2 +- render/tests/test-updateElement.js | 2 +- render/tests/test-updateFragment.js | 2 +- render/tests/test-updateHTML.js | 2 +- render/tests/test-updateNodes.js | 2 +- render/tests/test-updateNodesFuzzer.js | 2 +- render/tests/test-updateText.js | 2 +- 35 files changed, 54 insertions(+), 56 deletions(-) diff --git a/api/mount-redraw.js b/api/mount-redraw.js index 0cbecb1f5..163d252d3 100644 --- a/api/mount-redraw.js +++ b/api/mount-redraw.js @@ -4,7 +4,7 @@ var Vnode = require("../render/vnode") var coreRenderer = require("../render/render") module.exports = function($window, schedule, console) { - var renderService = coreRenderer($window) + var render = coreRenderer($window, redraw) var subscriptions = [] var rendering = false var pending = false @@ -13,11 +13,12 @@ module.exports = function($window, schedule, console) { if (rendering) throw new Error("Nested m.redraw.sync() call") rendering = true for (var i = 0; i < subscriptions.length; i += 2) { - try { renderService.render(subscriptions[i], Vnode(subscriptions[i + 1])) } + try { render(subscriptions[i], Vnode(subscriptions[i + 1])) } catch (e) { console.error(e) } } rendering = false } + function redraw() { if (!pending) { pending = true @@ -29,7 +30,6 @@ module.exports = function($window, schedule, console) { } redraw.sync = sync - renderService.setRedraw(redraw) function mount(root, component) { if (component != null && component.view == null && typeof component !== "function") { @@ -39,12 +39,12 @@ module.exports = function($window, schedule, console) { var index = subscriptions.indexOf(root) if (index >= 0) { subscriptions.splice(index, 2) - renderService.render(root, []) + render(root, []) } if (component != null) { subscriptions.push(root, component) - renderService.render(root, Vnode(component)) + render(root, Vnode(component)) } } diff --git a/api/router.js b/api/router.js index cc0795929..3357e7f8c 100644 --- a/api/router.js +++ b/api/router.js @@ -144,10 +144,10 @@ module.exports = function($window, mountRedraw) { onremove: onremove, view: function() { if (!state || sentinel === currentResolver) return - var vnode = Vnode(component, attrs.key, attrs) - if (currentResolver) vnode = currentResolver.render(vnode) // Wrap in a fragment to preserve existing key semantics - return [vnode] + var vnode = [Vnode(component, attrs.key, attrs)] + if (currentResolver) vnode = currentResolver.render(vnode[0]) + return vnode }, }) } diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 5ec418398..e89bdf580 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -708,7 +708,7 @@ o.spec("route", function() { }) }) - o("changing `vnode.key` in `render` resets the component", function(done){ + o("changing `key` param resets the component", function(done){ var oninit = o.spy() var Component = { oninit: oninit, @@ -718,9 +718,7 @@ o.spec("route", function() { } $window.location.href = prefix + "/abc" route(root, "/abc", { - "/:id": {render: function(vnode) { - return m(Component, {key: vnode.attrs.id}) - }} + "/:key": Component, }) callAsync(function() { o(oninit.callCount).equals(1) diff --git a/docs/change-log.md b/docs/change-log.md index 39348f54a..150aecad0 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -54,6 +54,10 @@ - Previously, numeric children weren't coerced. Now, they are. - Unlikely to break most components, but it *could* break some users. - This increases consistency with how booleans are handled with children, so it should be more intuitive. +- route: `key` parameter for routes now only works globally for components ([#????](https://github.com/MithrilJS/mithril.js/pull/????) [@isiahmeadows](https://github.com/isiahmeadows)) + - Previously, it worked for route resolvers, too. + - This lets you ensure global layouts used in `render` still render by diff. + #### News diff --git a/docs/hyperscript.md b/docs/hyperscript.md index f71ef3e7e..7bf438d44 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -454,7 +454,7 @@ var isError = false m("div", isError ? "An error occurred" : "Saved") //
Saved
``` -You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations. +You cannot use JavaScript statements such as `if` or `for` within JavaScript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative. --- diff --git a/docs/keys.md b/docs/keys.md index 8319350e7..de9076a43 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -75,7 +75,7 @@ function correctUserList(users) { } ``` -Also, you might want to reinitialize a component. You can use the common pattern of a single-item keyed fragment where you change the key to destroy and reinitialize the element. +Also, you might want to reinitialize a component. You can use the common pattern of a single-child keyed fragment where you change the key to destroy and reinitialize the element. ```javascript function ResettableToggle() { diff --git a/docs/route.md b/docs/route.md index 06caa4ceb..4b24d94b0 100644 --- a/docs/route.md +++ b/docs/route.md @@ -206,7 +206,7 @@ Argument | Type | Description #### How it works -Routing is a system that allows creating Single-Page-Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh. +Routing is a system that allows creating Single Page Applications (SPA), i.e. applications that can go from a "page" to another without causing a full browser refresh. It enables seamless navigability while preserving the ability to bookmark each page individually, and the ability to navigate the application via the browser's history mechanism. @@ -336,6 +336,8 @@ Or even use the [`history state`](#history-state) feature to achieve reloadable `m.route.set(m.route.get(), null, {state: {key: Date.now()}})` +Note that the key parameter works only for component routes. If you're using a route resolver, you'll need to use a [single-child keyed fragment](keys.md), passing `key: m.route.param("key")`, to accomplish the same. + #### Variadic routes It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes: diff --git a/performance/test-perf.js b/performance/test-perf.js index 020960c49..ee6db7547 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -44,7 +44,7 @@ if(!doc) { } var m = require("../render/hyperscript") -m.render = require("../render/render")(window).render +m.render = require("../render/render")(window) function resetScratch() { diff --git a/render.js b/render.js index 0e372a0a5..a6f8ce8d2 100644 --- a/render.js +++ b/render.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./render/render")(window).render +module.exports = require("./render/render")(window) diff --git a/render/render.js b/render/render.js index 0876ae37a..0b37c3caf 100644 --- a/render/render.js +++ b/render/render.js @@ -2,7 +2,7 @@ var Vnode = require("../render/vnode") -module.exports = function($window) { +module.exports = function($window, redraw) { var $doc = $window.document var nameSpace = { @@ -10,9 +10,6 @@ module.exports = function($window) { math: "http://www.w3.org/1998/Math/MathML" } - var redraw - function setRedraw(callback) {return redraw = callback} - function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } @@ -866,7 +863,7 @@ module.exports = function($window) { return true } - function render(dom, vnodes) { + return function(dom, vnodes) { if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = activeElement() @@ -882,6 +879,4 @@ module.exports = function($window) { if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus() for (var i = 0; i < hooks.length; i++) hooks[i]() } - - return {render: render, setRedraw: setRedraw} } diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 00eb80dcb..156323959 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -9,7 +9,7 @@ o.spec("attributes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.body - render = vdom($window).render + render = vdom($window) }) o.spec("basics", function() { o("works (create/update/remove)", function() { @@ -255,7 +255,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "input"} var b = {tag: "input", attrs: {value: "1"}} @@ -294,7 +294,7 @@ o.spec("attributes", function() { o("the input.type setter is never used", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "input", attrs: {type: "radio"}} var b = {tag: "input", attrs: {type: "text"}} @@ -334,7 +334,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value and focused", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "textarea"} var b = {tag: "textarea", attrs: {value: "1"}} @@ -480,7 +480,7 @@ o.spec("attributes", function() { o("isn't set when equivalent to the previous value", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = {tag: "option"} var b = {tag: "option", attrs: {value: "1"}} @@ -618,7 +618,7 @@ o.spec("attributes", function() { o("updates with the same value do not re-set the attribute if the select has focus", function() { var $window = domMock({spy: o.spy}) var root = $window.document.body - var render = vdom($window).render + var render = vdom($window) var a = makeSelect() var b = makeSelect("1") diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 9b2e90e2a..423ceeada 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -11,7 +11,7 @@ o.spec("component", function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) components.forEach(function(cmp){ diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js index 54119c68c..ba250a6ef 100644 --- a/render/tests/test-createElement.js +++ b/render/tests/test-createElement.js @@ -9,7 +9,7 @@ o.spec("createElement", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates element", function() { diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js index a40967a9c..5000f859e 100644 --- a/render/tests/test-createFragment.js +++ b/render/tests/test-createFragment.js @@ -9,7 +9,7 @@ o.spec("createFragment", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates fragment", function() { diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js index a337213b1..bf3055b2d 100644 --- a/render/tests/test-createHTML.js +++ b/render/tests/test-createHTML.js @@ -9,7 +9,7 @@ o.spec("createHTML", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates HTML", function() { diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js index 946f41a2a..649ebf9a9 100644 --- a/render/tests/test-createNodes.js +++ b/render/tests/test-createNodes.js @@ -9,7 +9,7 @@ o.spec("createNodes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates nodes", function() { diff --git a/render/tests/test-createText.js b/render/tests/test-createText.js index 73a371567..7a05c44ef 100644 --- a/render/tests/test-createText.js +++ b/render/tests/test-createText.js @@ -9,7 +9,7 @@ o.spec("createText", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("creates string", function() { diff --git a/render/tests/test-event.js b/render/tests/test-event.js index 74dcb75bc..379aef6ad 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -10,9 +10,8 @@ o.spec("event", function() { $window = domMock() root = $window.document.body redraw = o.spy() - var renderer = vdom($window) - renderer.setRedraw(redraw) - render = renderer.render + var renderer = vdom($window, redraw) + render = renderer }) o("handles onclick", function() { diff --git a/render/tests/test-input.js b/render/tests/test-input.js index c443db1a1..e28784a22 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -8,7 +8,7 @@ o.spec("form inputs", function() { var $window, root, render o.beforeEach(function() { $window = domMock() - render = vdom($window).render + render = vdom($window) root = $window.document.createElement("div") $window.document.body.appendChild(root) }) diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js index 86f75ffed..6ef57f6c3 100644 --- a/render/tests/test-normalizeComponentChildren.js +++ b/render/tests/test-normalizeComponentChildren.js @@ -8,7 +8,7 @@ var vdom = require("../../render/render") o.spec("component children", function () { var $window = domMock() var root = $window.document.createElement("div") - var render = vdom($window).render + var render = vdom($window) o.spec("component children", function () { var component = { diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 834f7510b..9e9a2e59e 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -12,7 +12,7 @@ o.spec("onbeforeremove", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onbeforeremove when creating", function() { diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 6c5adac8c..e179bad29 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -10,7 +10,7 @@ o.spec("onbeforeupdate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("prevents update in element", function() { diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index eb1daa5ec..df651f900 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -9,7 +9,7 @@ o.spec("oncreate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("calls oncreate when creating element", function() { diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index f6ffb873d..a42d51008 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -9,7 +9,7 @@ o.spec("oninit", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("calls oninit when creating element", function() { diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 6cc3fd5cd..bab0d3080 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -11,7 +11,7 @@ o.spec("onremove", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onremove when creating", function() { diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 4b74d288f..10794dd0c 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -9,7 +9,7 @@ o.spec("onupdate", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("does not call onupdate when creating element", function() { diff --git a/render/tests/test-render-hyperscript-integration.js b/render/tests/test-render-hyperscript-integration.js index 73d96a8f2..1b722fb05 100644 --- a/render/tests/test-render-hyperscript-integration.js +++ b/render/tests/test-render-hyperscript-integration.js @@ -10,7 +10,7 @@ o.spec("render/hyperscript integration", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o.spec("setting class", function() { o("selector only", function() { diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 33bd496a9..9236e9efa 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -9,7 +9,7 @@ o.spec("render", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("renders plain text", function() { diff --git a/render/tests/test-textContent.js b/render/tests/test-textContent.js index 9be5b4ba4..0cdd0b4ca 100644 --- a/render/tests/test-textContent.js +++ b/render/tests/test-textContent.js @@ -9,7 +9,7 @@ o.spec("textContent", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("ignores null", function() { diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index c1eb573f3..7a7285fab 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -9,7 +9,7 @@ o.spec("updateElement", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates attr", function() { diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js index 0204d935e..e0cdecf80 100644 --- a/render/tests/test-updateFragment.js +++ b/render/tests/test-updateFragment.js @@ -9,7 +9,7 @@ o.spec("updateFragment", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates fragment", function() { diff --git a/render/tests/test-updateHTML.js b/render/tests/test-updateHTML.js index c678e5391..294456e9d 100644 --- a/render/tests/test-updateHTML.js +++ b/render/tests/test-updateHTML.js @@ -9,7 +9,7 @@ o.spec("updateHTML", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates html", function() { diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index f6bf36682..5ce9e32f5 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -10,7 +10,7 @@ o.spec("updateNodes", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("handles el noop", function() { diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js index 8580c1392..c5ad44d0a 100644 --- a/render/tests/test-updateNodesFuzzer.js +++ b/render/tests/test-updateNodesFuzzer.js @@ -10,7 +10,7 @@ o.spec("updateNodes keyed list Fuzzer", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) diff --git a/render/tests/test-updateText.js b/render/tests/test-updateText.js index 86618e12e..f8579dead 100644 --- a/render/tests/test-updateText.js +++ b/render/tests/test-updateText.js @@ -9,7 +9,7 @@ o.spec("updateText", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") - render = vdom($window).render + render = vdom($window) }) o("updates to string", function() { From 35a34eb290a584f3044176ba247d21d32806991f Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 17:52:37 -0400 Subject: [PATCH 5/7] Add `redraw` to `m.render`, deservicify requests --- api/mount-redraw.js | 10 ++++------ api/tests/test-mountRedraw.js | 19 +++++-------------- api/tests/test-router.js | 6 +++--- api/tests/test-routerGetSet.js | 4 +++- docs/change-log.md | 3 ++- docs/render.md | 11 +++++++---- index.js | 13 +++++-------- mount-redraw.js | 4 +++- render/render.js | 21 +++++++++++++++------ render/tests/test-event.js | 6 ++++-- request.js | 4 +++- request/request.js | 6 +----- request/tests/test-jsonp.js | 6 ++---- request/tests/test-request.js | 9 ++------- 14 files changed, 59 insertions(+), 63 deletions(-) diff --git a/api/mount-redraw.js b/api/mount-redraw.js index 163d252d3..d51da61b7 100644 --- a/api/mount-redraw.js +++ b/api/mount-redraw.js @@ -1,10 +1,8 @@ "use strict" var Vnode = require("../render/vnode") -var coreRenderer = require("../render/render") -module.exports = function($window, schedule, console) { - var render = coreRenderer($window, redraw) +module.exports = function(render, schedule, console) { var subscriptions = [] var rendering = false var pending = false @@ -13,7 +11,7 @@ module.exports = function($window, schedule, console) { 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])) } + try { render(subscriptions[i], Vnode(subscriptions[i + 1]), redraw) } catch (e) { console.error(e) } } rendering = false @@ -39,12 +37,12 @@ module.exports = function($window, schedule, console) { var index = subscriptions.indexOf(root) if (index >= 0) { subscriptions.splice(index, 2) - render(root, []) + render(root, [], redraw) } if (component != null) { subscriptions.push(root, component) - render(root, Vnode(component)) + render(root, Vnode(component), redraw) } } diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index 7cb4432f6..df118c187 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -1,25 +1,14 @@ "use strict" +// Low-priority TODO: remove the dependency on the renderer here. var o = require("../../ospec/ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var throttleMocker = require("../../test-utils/throttleMock") var mountRedraw = require("../../api/mount-redraw") +var coreRenderer = require("../../render/render") var h = require("../../render/hyperscript") -// Because Node doesn't have this. -if (typeof requestAnimationFrame !== "function") { - global.requestAnimationFrame = (function(delay, last) { - return function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - } - })(16, 0) -} - o.spec("mount/redraw", function() { var root, m, throttleMock, consoleMock, $document, errors o.beforeEach(function() { @@ -27,7 +16,7 @@ o.spec("mount/redraw", function() { consoleMock = {error: o.spy()} throttleMock = throttleMocker() root = $window.document.body - m = mountRedraw($window, throttleMock.schedule, consoleMock) + m = mountRedraw(coreRenderer($window), throttleMock.schedule, consoleMock) $document = $window.document errors = [] }) @@ -380,10 +369,12 @@ o.spec("mount/redraw", function() { root.firstChild.dispatchEvent(e) o(oninit.callCount).equals(1) + o(e.redraw).equals(false) throttleMock.fire() o(onupdate.callCount).equals(0) + o(e.redraw).equals(false) }) o("redraws when the render function is run", function() { diff --git a/api/tests/test-router.js b/api/tests/test-router.js index e89bdf580..b0d36bab4 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1,11 +1,13 @@ "use strict" +// Low-priority TODO: remove the dependency on the renderer here. var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") +var coreRenderer = require("../../render/render") var callAsync = require("../../test-utils/callAsync") var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") @@ -23,7 +25,7 @@ o.spec("route", function() { root = $window.document.body - mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) route = apiRouter($window, mountRedraw) route.prefix(prefix) }) @@ -417,8 +419,6 @@ o.spec("route", function() { root.firstChild.dispatchEvent(e) - o(e.redraw).notEquals(false) - // Wrapped to ensure no redraw fired callAsync(function() { o(onupdate.callCount).equals(0) diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index d507b97b8..cd23ae200 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -1,5 +1,6 @@ "use strict" +// Low-priority TODO: remove the dependency on the renderer here. var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") var browserMock = require("../../test-utils/browserMock") @@ -7,6 +8,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var callAsync = require("../../test-utils/callAsync") var apiMountRedraw = require("../../api/mount-redraw") +var coreRenderer = require("../../render/render") var apiRouter = require("../../api/router") o.spec("route.get/route.set", function() { @@ -21,7 +23,7 @@ o.spec("route.get/route.set", function() { root = $window.document.body - mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) route = apiRouter($window, mountRedraw) route.prefix(prefix) }) diff --git a/docs/change-log.md b/docs/change-log.md index 150aecad0..9ccc39204 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -54,7 +54,7 @@ - Previously, numeric children weren't coerced. Now, they are. - Unlikely to break most components, but it *could* break some users. - This increases consistency with how booleans are handled with children, so it should be more intuitive. -- route: `key` parameter for routes now only works globally for components ([#????](https://github.com/MithrilJS/mithril.js/pull/????) [@isiahmeadows](https://github.com/isiahmeadows)) +- route: `key` parameter for routes now only works globally for components ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) - Previously, it worked for route resolvers, too. - This lets you ensure global layouts used in `render` still render by diff. @@ -85,6 +85,7 @@ - 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.) +- render: new `redraw` parameter exposed any time a child event handler is used ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) #### Bug fixes diff --git a/docs/render.md b/docs/render.md index 0e2af7288..030e7821c 100644 --- a/docs/render.md +++ b/docs/render.md @@ -22,12 +22,13 @@ m.render(document.body, "hello") ### Signature -`m.render(element, vnodes)` +`m.render(element, vnodes, redraw)` Argument | Type | Required | Description ----------- | -------------------- | -------- | --- `element` | `Element` | Yes | A DOM element that will be the parent node to the subtree `vnodes` | `Array|Vnode` | Yes | The [vnodes](vnodes.md) to be rendered +`redraw` | `() -> any` | No | A callback invoked each time an event handler in the subtree is invoked **returns** | | | Returns `undefined` [How to read signatures](signatures.md) @@ -36,7 +37,9 @@ Argument | Type | Required | Description ### How it works -The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. +The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. + +If you pass the optional `redraw` argument, that is invoked each time an event handler anywhere in the subtree is called. This is used by [`m.mount`](mount.md) and [`m.redraw`](redraw.md) to implement the autoredraw mechanism, but is also exposed for convenience and third-party integration in advanced use cases. `m.render` is synchronous. @@ -66,6 +69,6 @@ Another difference is that `m.render` method expects a [vnode](vnodes.md) (or a `var render = require("mithril/render")` -The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It is approximately 500 lines of code (3kb min+gzip) and implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril and can be used as a standalone library. +The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril aside from normalization exposed via `require("mithril/render/vnode")` and can be used as a standalone library. -Despite being incredibly small, the render module is fully functional and self-sufficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). +Despite being relatively small, the render module is fully functional and self-sufficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). diff --git a/index.js b/index.js index 3756e6795..4fdf3d9e6 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,19 @@ "use strict" var hyperscript = require("./hyperscript") +var request = require("./request") +var mountRedraw = require("./mount-redraw") + var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript m.trust = hyperscript.trust m.fragment = hyperscript.fragment - -var requestService = require("./request") -var mountRedraw = require("./mount-redraw") - -requestService.setCompletionCallback(mountRedraw.redraw) - m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render") m.redraw = mountRedraw.redraw -m.request = requestService.request -m.jsonp = requestService.jsonp +m.request = request.request +m.jsonp = request.jsonp m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.parsePathname = require("./pathname/parse") diff --git a/mount-redraw.js b/mount-redraw.js index bcdf52ef7..8cd149ec8 100644 --- a/mount-redraw.js +++ b/mount-redraw.js @@ -1,3 +1,5 @@ "use strict" -module.exports = require("./api/mount-redraw")(window, requestAnimationFrame, console) +var render = require("./render") + +module.exports = require("./api/mount-redraw")(render, requestAnimationFrame, console) diff --git a/render/render.js b/render/render.js index 0b37c3caf..64d375b35 100644 --- a/render/render.js +++ b/render/render.js @@ -2,8 +2,9 @@ var Vnode = require("../render/vnode") -module.exports = function($window, redraw) { +module.exports = function($window) { var $doc = $window.document + var currentRedraw var nameSpace = { svg: "http://www.w3.org/2000/svg", @@ -794,15 +795,17 @@ module.exports = function($window, redraw) { // that below. // 6. In function-based event handlers, `return false` prevents the default // action and stops event propagation. We replicate that below. - function EventDict() {} + function EventDict() { + // Save this, so the current redraw is correctly tracked. + this._ = currentRedraw + } EventDict.prototype = Object.create(null) EventDict.prototype.handleEvent = function (ev) { var handler = this["on" + ev.type] var result if (typeof handler === "function") result = handler.call(ev.currentTarget, ev) else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) - if (ev.redraw === false) ev.redraw = undefined - else if (typeof redraw === "function") redraw() + if (this._ && ev.redraw !== false) (0, this._)() if (result === false) { ev.preventDefault() ev.stopPropagation() @@ -863,7 +866,7 @@ module.exports = function($window, redraw) { return true } - return function(dom, vnodes) { + return function(dom, vnodes, redraw) { if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = activeElement() @@ -873,7 +876,13 @@ module.exports = function($window, redraw) { if (dom.vnodes == null) dom.textContent = "" vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]) - updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + var prevRedraw = currentRedraw + try { + currentRedraw = typeof redraw === "function" ? redraw : undefined + updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) + } finally { + currentRedraw = prevRedraw + } dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus() diff --git a/render/tests/test-event.js b/render/tests/test-event.js index 379aef6ad..381b4276a 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -10,8 +10,10 @@ o.spec("event", function() { $window = domMock() root = $window.document.body redraw = o.spy() - var renderer = vdom($window, redraw) - render = renderer + var renderer = vdom($window) + render = function(dom, vnode) { + return renderer(dom, vnode, redraw) + } }) o("handles onclick", function() { diff --git a/request.js b/request.js index cb3bfbbe4..06fb2bbd5 100644 --- a/request.js +++ b/request.js @@ -1,4 +1,6 @@ "use strict" var PromisePolyfill = require("./promise/promise") -module.exports = require("./request/request")(window, PromisePolyfill) +var mountRedraw = require("./mount-redraw") + +module.exports = require("./request/request")(window, PromisePolyfill, mountRedraw.redraw) diff --git a/request/request.js b/request/request.js index 48f52f39e..0e267769c 100644 --- a/request/request.js +++ b/request/request.js @@ -2,9 +2,8 @@ var buildPathname = require("../pathname/build") -module.exports = function($window, Promise) { +module.exports = function($window, Promise, oncompletion) { var callbackCount = 0 - var oncompletion function PromiseProxy(executor) { return new Promise(executor) @@ -191,8 +190,5 @@ module.exports = function($window, Promise) { encodeURIComponent(callbackName) $window.document.documentElement.appendChild(script) }), - setCompletionCallback: function(callback) { - oncompletion = callback - }, } } diff --git a/request/tests/test-jsonp.js b/request/tests/test-jsonp.js index 58c7f08b6..6dfed4e7c 100644 --- a/request/tests/test-jsonp.js +++ b/request/tests/test-jsonp.js @@ -3,17 +3,15 @@ var o = require("../../ospec/ospec") var xhrMock = require("../../test-utils/xhrMock") var Request = require("../../request/request") -var Promise = require("../../promise/promise") +var PromisePolyfill = require("../../promise/promise") var parseQueryString = require("../../querystring/parse") o.spec("jsonp", function() { var mock, jsonp, complete o.beforeEach(function() { mock = xhrMock() - var requestService = Request(mock, Promise) - jsonp = requestService.jsonp complete = o.spy() - requestService.setCompletionCallback(complete) + jsonp = Request(mock, PromisePolyfill, complete).jsonp }) o("works", function(done) { diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 969de68b5..706c8404b 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -10,10 +10,8 @@ o.spec("request", function() { var mock, request, complete o.beforeEach(function() { mock = xhrMock() - var requestService = Request(mock, PromisePolyfill) - request = requestService.request complete = o.spy() - requestService.setCompletionCallback(complete) + request = Request(mock, PromisePolyfill, complete).request }) o.spec("success", function() { @@ -835,10 +833,7 @@ o.spec("request", function() { // 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) + request = Request(mock, Promise, complete).request mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: "[]"} From b9ba19e0150b5cd22db8d783ea137da2cf42b9aa Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 18:01:36 -0400 Subject: [PATCH 6/7] Test error logging --- api/tests/test-mountRedraw.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js index df118c187..0c52379c8 100644 --- a/api/tests/test-mountRedraw.js +++ b/api/tests/test-mountRedraw.js @@ -399,6 +399,26 @@ o.spec("mount/redraw", function() { o(onupdate.callCount).equals(1) }) + + o("emits errors correctly", function() { + errors = ["foo", "bar", "baz"] + var counter = -1 + + m.mount(root, createComponent({ + view: function() { + var value = errors[counter++] + if (value != null) throw value + return null + } + })) + + m.redraw() + throttleMock.fire() + m.redraw() + throttleMock.fire() + m.redraw() + throttleMock.fire() + }) }) }) }) From 221328d1039366b9b998bfabb30f81e57b7f3dff Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sun, 7 Jul 2019 18:14:27 -0400 Subject: [PATCH 7/7] Update docs + changelog [skip ci] --- docs/change-log.md | 3 +++ docs/mount.md | 30 ++++++++++++++++++++++++++++++ docs/render.md | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index 9ccc39204..cf3e00f9d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -57,6 +57,9 @@ - route: `key` parameter for routes now only works globally for components ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) - Previously, it worked for route resolvers, too. - This lets you ensure global layouts used in `render` still render by diff. +- redraw: `mithril/redraw` now just exposes the `m.redraw` callback ([#2458](https://github.com/MithrilJS/mithril.js/pull/2458) [@isiahmeadows](https://github.com/isiahmeadows)) + - The `.schedule`, `.unschedule`, and `.render` properties of the former `redrawService` are all removed. + - If you want to know how to work around it, look at the call to `mount` in Mithril's source for `m.route`. That should help you in finding ways around the removed feature. (It doesn't take that much more code.) #### News diff --git a/docs/mount.md b/docs/mount.md index eacb03b7e..5263ccdb5 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -3,6 +3,7 @@ - [Description](#description) - [Signature](#signature) - [How it works](#how-it-works) +- [Headless mounts](#headless-mounts) - [Performance considerations](#performance-considerations) - [Differences from m.render](#differences-from-mrender) @@ -61,6 +62,35 @@ Running `mount(element, OtherComponent)` where `element` is a current mount poin Using `m.mount(element, null)` on an element with a previously mounted component unmounts it and cleans up Mithril internal state. This can be useful to prevent memory leaks when removing the `root` node manually from the DOM. +#### Headless mounts + +In certain more advanced situations, you may want to subscribe and listen for [redraws](autoredraw.md) without rendering anything to the screen. This can be done using a headless mount, created by simply invoking `m.mount` with an element that's not added to the live DOM tree and putting all your useful logic in the component you're mounting with. You still need a `view` in your component, just it doesn't have to return anything useful and it can just return a junk value like `null` or `undefined`. + +```javascript +var elem = document.createElement("div") + +// Subscribe +m.mount(elem, { + oncreate: function() { + // once added + }, + onupdate: function() { + // on each redraw + }, + onremove: function() { + // clean up whatever you need + }, + + // Necessary boilerplate + view: function () {}, +}) + +// Unsubscribe +m.mount(elem, null) +``` + +There's no need to worry about other mount roots. Multiple roots are supported and they won't step on each other. You can even do the above in a component when integrating with another framework, and it won't be a problem. + --- ### Performance considerations diff --git a/docs/render.md b/docs/render.md index 030e7821c..36516d834 100644 --- a/docs/render.md +++ b/docs/render.md @@ -39,7 +39,7 @@ Argument | Type | Required | Description The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. -If you pass the optional `redraw` argument, that is invoked each time an event handler anywhere in the subtree is called. This is used by [`m.mount`](mount.md) and [`m.redraw`](redraw.md) to implement the autoredraw mechanism, but is also exposed for convenience and third-party integration in advanced use cases. +If you pass the optional `redraw` argument, that is invoked each time an event handler anywhere in the subtree is called. This is used by [`m.mount`](mount.md) and [`m.redraw`](redraw.md) to implement the [autoredraw](autoredraw.md) functionality, but it's also exposed for more advanced use cases like integration with some third-party frameworks. `m.render` is synchronous.