diff --git a/api/mount-redraw.js b/api/mount-redraw.js new file mode 100644 index 000000000..d51da61b7 --- /dev/null +++ b/api/mount-redraw.js @@ -0,0 +1,50 @@ +"use strict" + +var Vnode = require("../render/vnode") + +module.exports = function(render, schedule, console) { + var subscriptions = [] + var rendering = false + var pending = false + + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 0; i < subscriptions.length; i += 2) { + try { render(subscriptions[i], Vnode(subscriptions[i + 1]), redraw) } + catch (e) { console.error(e) } + } + rendering = false + } + + function redraw() { + if (!pending) { + pending = true + schedule(function() { + pending = false + sync() + }) + } + } + + redraw.sync = sync + + function mount(root, component) { + if (component != null && component.view == null && typeof component !== "function") { + throw new TypeError("m.mount(element, component) expects a component, not a vnode") + } + + var index = subscriptions.indexOf(root) + if (index >= 0) { + subscriptions.splice(index, 2) + render(root, [], redraw) + } + + if (component != null) { + subscriptions.push(root, component) + render(root, Vnode(component), redraw) + } + } + + return {mount: mount, redraw: redraw} +} diff --git a/api/mount.js b/api/mount.js deleted file mode 100644 index ab8ecc137..000000000 --- a/api/mount.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(redrawService) { - return function(root, component) { - if (component === null) { - redrawService.unsubscribe(root) - } else if (component.view == null && typeof component !== "function") { - throw new Error("m.mount(element, component) expects a component, not a vnode") - } else { - redrawService.subscribe(root, function() { return Vnode(component) }) - } - } -} diff --git a/api/redraw.js b/api/redraw.js deleted file mode 100644 index 235157f33..000000000 --- a/api/redraw.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict" - -var coreRenderer = require("../render/render") - -function throttle(callback) { - var pending = null - return function() { - if (pending === null) { - pending = requestAnimationFrame(function() { - pending = null - callback() - }) - } - } -} - -module.exports = function($window, throttleMock) { - var renderService = coreRenderer($window) - var subscriptions = [] - var rendering = false - - function run(sub) { - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function subscribe(key, callback, onremove) { - var sub = {k: key, c: callback, r: onremove} - unsubscribe(key) - subscriptions.push(sub) - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function unsubscribe(key) { - for (var i = 0; i < subscriptions.length; i++) { - var sub = subscriptions[i] - if (sub.k === key) { - subscriptions.splice(i, 1) - renderService.render(sub.k, []) - if (typeof sub.r === "function") sub.r() - break - } - } - } - function sync() { - if (rendering) throw new Error("Nested m.redraw.sync() call") - rendering = true - for (var i = 0; i < subscriptions.length; i++) { - try { run(subscriptions[i]) } - catch (e) { if (typeof console !== "undefined") console.error(e) } - } - rendering = false - } - - var redraw = (throttleMock || throttle)(sync) - redraw.sync = sync - renderService.setRedraw(redraw) - return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} -} diff --git a/api/router.js b/api/router.js index a388b40cf..3357e7f8c 100644 --- a/api/router.js +++ b/api/router.js @@ -2,55 +2,153 @@ var Vnode = require("../render/vnode") var Promise = require("../promise/promise") -var coreRouter = require("../router/router") + +var buildPathname = require("../pathname/build") +var parsePathname = require("../pathname/parse") +var compileTemplate = require("../pathname/compileTemplate") +var assign = require("../pathname/assign") var sentinel = {} -module.exports = function($window, redrawService) { - var routeService = coreRouter($window) +module.exports = function($window, mountRedraw) { + var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout + var supportsPushState = typeof $window.history.pushState === "function" + var routePrefix = "#!" + var fireAsync + + function setPath(path, data, options) { + path = buildPathname(path, data) + if (fireAsync != null) { + fireAsync() + var state = options ? options.state : null + var title = options ? options.title : null + if (options && options.replace) $window.history.replaceState(state, title, routePrefix + path) + else $window.history.pushState(state, title, routePrefix + path) + } + else { + $window.location.href = routePrefix + path + } + } var currentResolver = sentinel, component, attrs, currentPath, lastUpdate var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") - var init = false - var bail = function(path) { - if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) - else throw new Error("Could not resolve default route " + defaultRoute) - } - function run() { - init = true - if (sentinel !== currentResolver) { - var vnode = Vnode(component, attrs.key, attrs) - if (currentResolver) vnode = currentResolver.render(vnode) - return vnode + // 0 = start + // 1 = init + // 2 = ready + var state = 0 + + var compiled = Object.keys(routes).map(function(route) { + if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`") + if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) { + throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`") + } + return { + route: route, + component: routes[route], + check: compileTemplate(route), + } + }) + var onremove, asyncId + + fireAsync = null + + if (defaultRoute != null) { + var defaultData = parsePathname(defaultRoute) + + if (!compiled.some(function (i) { return i.check(defaultData) })) { + throw new ReferenceError("Default route doesn't match any known routes") } } - routeService.defineRoutes(routes, function(payload, params, path, route) { - var update = lastUpdate = function(routeResolver, comp) { - if (update !== lastUpdate) return - component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" - attrs = params, currentPath = path, lastUpdate = null - currentResolver = routeResolver.render ? routeResolver : null - if (init) redrawService.redraw() - else { - init = true - redrawService.redraw.sync() + + function resolveRoute() { + // Consider the pathname holistically. The prefix might even be invalid, + // but that's not our problem. + var prefix = $window.location.hash + if (routePrefix[0] !== "#") { + prefix = $window.location.search + prefix + if (routePrefix[0] !== "?") { + prefix = $window.location.pathname + prefix + if (prefix[0] !== "/") prefix = "/" + prefix } } - if (payload.view || typeof payload === "function") update({}, payload) - else { - if (payload.onmatch) { - Promise.resolve(payload.onmatch(params, path, route)).then(function(resolved) { - update(payload, resolved) - }, function () { bail(path) }) + // This seemingly useless `.concat()` speeds up the tests quite a bit, + // since the representation is consistently a relatively poorly + // optimized cons string. + var path = prefix.concat() + .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) + .slice(routePrefix.length) + var data = parsePathname(path) + + assign(data.params, $window.history.state) + + for (var i = 0; i < compiled.length; i++) { + if (compiled[i].check(data)) { + var payload = compiled[i].component + var route = compiled[i].route + var update = lastUpdate = function(routeResolver, comp) { + if (update !== lastUpdate) return + component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + attrs = data.params, currentPath = path, lastUpdate = null + currentResolver = routeResolver.render ? routeResolver : null + if (state === 2) mountRedraw.redraw() + else { + state = 2 + mountRedraw.redraw.sync() + } + } + if (payload.view || typeof payload === "function") update({}, payload) + else { + if (payload.onmatch) { + Promise.resolve(payload.onmatch(data.params, path, route)).then(function(resolved) { + update(payload, resolved) + }, function () { + if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute) + setPath(defaultRoute, null, {replace: true}) + }) + } + else update(payload, "div") + } + return } - else update(payload, "div") } - }, bail, defaultRoute, function (unsubscribe) { - redrawService.subscribe(root, function(sub) { - sub.c = run - return sub - }, unsubscribe) + + if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute) + setPath(defaultRoute, null, {replace: true}) + } + + if (supportsPushState) { + onremove = function() { + $window.removeEventListener("popstate", fireAsync, false) + } + $window.addEventListener("popstate", fireAsync = function() { + if (asyncId) return + asyncId = callAsync(function() { + asyncId = null + resolveRoute() + }) + }, false) + } else if (routePrefix[0] === "#") { + onremove = function() { + $window.removeEventListener("hashchange", resolveRoute, false) + } + $window.addEventListener("hashchange", resolveRoute, false) + } + + return mountRedraw.mount(root, { + onbeforeupdate: function() { + state = state ? 2 : 1 + return !(!state || sentinel === currentResolver) + }, + oncreate: resolveRoute, + onremove: onremove, + view: function() { + if (!state || sentinel === currentResolver) return + // Wrap in a fragment to preserve existing key semantics + var vnode = [Vnode(component, attrs.key, attrs)] + if (currentResolver) vnode = currentResolver.render(vnode[0]) + return vnode + }, }) } route.set = function(path, data, options) { @@ -59,18 +157,18 @@ module.exports = function($window, redrawService) { options.replace = true } lastUpdate = null - routeService.setPath(path, data, options) + setPath(path, data, options) } route.get = function() {return currentPath} - route.prefix = function(prefix) {routeService.prefix = prefix} + route.prefix = function(prefix) {routePrefix = prefix} var link = function(options, vnode) { - vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href) + vnode.dom.setAttribute("href", routePrefix + vnode.attrs.href) vnode.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return e.preventDefault() e.redraw = false var href = this.getAttribute("href") - if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) + if (href.indexOf(routePrefix) === 0) href = href.slice(routePrefix.length) route.set(href, undefined, options) } } diff --git a/api/tests/index.html b/api/tests/index.html index e85bd4b7e..e7d28bdbc 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -24,13 +24,11 @@ - - - + - - + + 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..0c52379c8 --- /dev/null +++ b/api/tests/test-mountRedraw.js @@ -0,0 +1,424 @@ +"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") + +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(coreRenderer($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) + 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() { + 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) + }) + + 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() + }) + }) + }) +}) 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 1f7fc1d9a..b0d36bab4 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -1,13 +1,15 @@ "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 apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -15,7 +17,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) @@ -23,8 +25,8 @@ o.spec("route", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService) + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) @@ -55,6 +57,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() @@ -63,7 +224,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -83,7 +244,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -102,7 +263,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -124,8 +285,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) }) @@ -192,7 +352,7 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(onupdate.callCount).equals(1) @@ -259,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) @@ -550,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, @@ -560,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) @@ -661,7 +817,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -697,7 +853,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -1022,7 +1178,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) @@ -1286,10 +1442,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 new file mode 100644 index 000000000..cd23ae200 --- /dev/null +++ b/api/tests/test-routerGetSet.js @@ -0,0 +1,282 @@ +"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 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() { + 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, mountRedraw, route, throttleMock + + o.beforeEach(function() { + $window = browserMock(env) + throttleMock = throttleMocker() + + root = $window.document.body + + mountRedraw = apiMountRedraw(coreRenderer($window), throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) + 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/docs/change-log.md b/docs/change-log.md index 39348f54a..cf3e00f9d 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -54,6 +54,13 @@ - 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 ([#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 @@ -81,6 +88,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/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/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/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/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 0e2af7288..36516d834 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](autoredraw.md) functionality, but it's also exposed for more advanced use cases like integration with some third-party frameworks. `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/docs/route.md b/docs/route.md index 787ab0a83..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: @@ -739,7 +741,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..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 redrawService = require("./redraw") - -requestService.setCompletionCallback(redrawService.redraw) - -m.mount = require("./mount") +m.mount = mountRedraw.mount m.route = require("./route") -m.render = require("./render").render -m.redraw = redrawService.redraw -m.request = requestService.request -m.jsonp = requestService.jsonp +m.render = require("./render") +m.redraw = mountRedraw.redraw +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 new file mode 100644 index 000000000..8cd149ec8 --- /dev/null +++ b/mount-redraw.js @@ -0,0 +1,5 @@ +"use strict" + +var render = require("./render") + +module.exports = require("./api/mount-redraw")(render, 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/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/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..64d375b35 100644 --- a/render/render.js +++ b/render/render.js @@ -4,15 +4,13 @@ var Vnode = require("../render/vnode") module.exports = function($window) { var $doc = $window.document + var currentRedraw var nameSpace = { svg: "http://www.w3.org/2000/svg", 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] } @@ -797,15 +795,17 @@ module.exports = function($window) { // 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() @@ -866,8 +866,8 @@ module.exports = function($window) { return true } - 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.") + 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() var namespace = dom.namespaceURI @@ -876,12 +876,16 @@ module.exports = function($window) { 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() 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..381b4276a 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -11,8 +11,9 @@ o.spec("event", function() { root = $window.document.body redraw = o.spy() var renderer = vdom($window) - renderer.setRedraw(redraw) - render = renderer.render + render = function(dom, vnode) { + return renderer(dom, vnode, redraw) + } }) 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() { 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: "[]"} diff --git a/route.js b/route.js index 4e8295276..9cf121e23 100644 --- a/route.js +++ b/route.js @@ -1,5 +1,5 @@ "use strict" -var redrawService = require("./redraw") +var mountRedraw = require("./mount-redraw") -module.exports = require("./api/router")(window, redrawService) +module.exports = require("./api/router")(window, mountRedraw) 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() - }) - }) - }) - }) - }) -}) 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