From 755ddb1f9bd8c1c392acbe812d4ae002919c7b05 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Sat, 6 Jul 2019 16:14:20 -0400 Subject: [PATCH] Merge `m.mount` + `m.redraw`, update router Simplifies the router and redraw mechanism, and makes it much easier to keep predictable. Bundle size down to 9433 bytes min+gzip, docs updated accordingly. --- api/mount-redraw.js | 52 ++++ api/mount.js | 15 - api/redraw.js | 58 ---- api/router.js | 8 +- api/tests/index.html | 6 +- api/tests/test-mount.js | 274 ----------------- api/tests/test-mountRedraw.js | 413 ++++++++++++++++++++++++++ api/tests/test-redraw.js | 195 ------------ api/tests/test-router.js | 34 +-- api/tests/test-routerGetSet.js | 9 +- docs/index.md | 2 +- docs/route.md | 2 +- index.js | 8 +- mount-redraw.js | 3 + mount.js | 4 +- redraw.js | 2 +- render/render.js | 2 +- route.js | 5 +- test-utils/tests/test-throttleMock.js | 63 +--- test-utils/throttleMock.js | 13 +- 20 files changed, 512 insertions(+), 656 deletions(-) create mode 100644 api/mount-redraw.js delete mode 100644 api/mount.js delete mode 100644 api/redraw.js delete mode 100644 api/tests/test-mount.js create mode 100644 api/tests/test-mountRedraw.js delete mode 100644 api/tests/test-redraw.js create mode 100644 mount-redraw.js diff --git a/api/mount-redraw.js b/api/mount-redraw.js new file mode 100644 index 000000000..0cbecb1f5 --- /dev/null +++ b/api/mount-redraw.js @@ -0,0 +1,52 @@ +"use strict" + +var Vnode = require("../render/vnode") +var coreRenderer = require("../render/render") + +module.exports = function($window, schedule, console) { + var renderService = coreRenderer($window) + var subscriptions = [] + var rendering = false + var pending = false + + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 0; i < subscriptions.length; i += 2) { + try { renderService.render(subscriptions[i], Vnode(subscriptions[i + 1])) } + catch (e) { console.error(e) } + } + rendering = false + } + function redraw() { + if (!pending) { + pending = true + schedule(function() { + pending = false + sync() + }) + } + } + + redraw.sync = sync + renderService.setRedraw(redraw) + + function mount(root, component) { + if (component != null && component.view == null && typeof component !== "function") { + throw new TypeError("m.mount(element, component) expects a component, not a vnode") + } + + var index = subscriptions.indexOf(root) + if (index >= 0) { + subscriptions.splice(index, 2) + renderService.render(root, []) + } + + if (component != null) { + subscriptions.push(root, component) + renderService.render(root, Vnode(component)) + } + } + + return {mount: mount, redraw: redraw} +} diff --git a/api/mount.js b/api/mount.js deleted file mode 100644 index ab8ecc137..000000000 --- a/api/mount.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -var Vnode = require("../render/vnode") - -module.exports = function(redrawService) { - return function(root, component) { - if (component === null) { - redrawService.unsubscribe(root) - } else if (component.view == null && typeof component !== "function") { - throw new Error("m.mount(element, component) expects a component, not a vnode") - } else { - redrawService.subscribe(root, function() { return Vnode(component) }) - } - } -} diff --git a/api/redraw.js b/api/redraw.js deleted file mode 100644 index 235157f33..000000000 --- a/api/redraw.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict" - -var coreRenderer = require("../render/render") - -function throttle(callback) { - var pending = null - return function() { - if (pending === null) { - pending = requestAnimationFrame(function() { - pending = null - callback() - }) - } - } -} - -module.exports = function($window, throttleMock) { - var renderService = coreRenderer($window) - var subscriptions = [] - var rendering = false - - function run(sub) { - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function subscribe(key, callback, onremove) { - var sub = {k: key, c: callback, r: onremove} - unsubscribe(key) - subscriptions.push(sub) - var vnode = sub.c(sub) - if (vnode !== sub) renderService.render(sub.k, vnode) - } - function unsubscribe(key) { - for (var i = 0; i < subscriptions.length; i++) { - var sub = subscriptions[i] - if (sub.k === key) { - subscriptions.splice(i, 1) - renderService.render(sub.k, []) - if (typeof sub.r === "function") sub.r() - break - } - } - } - function sync() { - if (rendering) throw new Error("Nested m.redraw.sync() call") - rendering = true - for (var i = 0; i < subscriptions.length; i++) { - try { run(subscriptions[i]) } - catch (e) { if (typeof console !== "undefined") console.error(e) } - } - rendering = false - } - - var redraw = (throttleMock || throttle)(sync) - redraw.sync = sync - renderService.setRedraw(redraw) - return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} -} diff --git a/api/router.js b/api/router.js index 166467fca..cc0795929 100644 --- a/api/router.js +++ b/api/router.js @@ -10,7 +10,7 @@ var assign = require("../pathname/assign") var sentinel = {} -module.exports = function($window, redrawService, mount) { +module.exports = function($window, mountRedraw) { var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout var supportsPushState = typeof $window.history.pushState === "function" var routePrefix = "#!" @@ -91,10 +91,10 @@ module.exports = function($window, redrawService, mount) { component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" attrs = data.params, currentPath = path, lastUpdate = null currentResolver = routeResolver.render ? routeResolver : null - if (state === 2) redrawService.redraw() + if (state === 2) mountRedraw.redraw() else { state = 2 - redrawService.redraw.sync() + mountRedraw.redraw.sync() } } if (payload.view || typeof payload === "function") update({}, payload) @@ -135,7 +135,7 @@ module.exports = function($window, redrawService, mount) { $window.addEventListener("hashchange", resolveRoute, false) } - return mount(root, { + return mountRedraw.mount(root, { onbeforeupdate: function() { state = state ? 2 : 1 return !(!state || sentinel === currentResolver) diff --git a/api/tests/index.html b/api/tests/index.html index 976f74124..e7d28bdbc 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -24,11 +24,9 @@ - - + - - + diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js deleted file mode 100644 index 7385e43d5..000000000 --- a/api/tests/test-mount.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var components = require("../../test-utils/components") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") - -var m = require("../../render/hyperscript") -var apiRedraw = require("../../api/redraw") -var apiMounter = require("../../api/mount") - -o.spec("mount", function() { - var $window, root, redrawService, mount, render, throttleMock - - o.beforeEach(function() { - $window = domMock() - throttleMock = throttleMocker() - - root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - mount = apiMounter(redrawService) - render = redrawService.render - }) - - o.afterEach(function() { - o(throttleMock.queueLength()).equals(0) - }) - - o("throws on invalid component", function() { - var threw = false - try { - mount(root, {}) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - components.forEach(function(cmp){ - o.spec(cmp.kind, function(){ - var createComponent = cmp.create - - o("throws on invalid `root` DOM node", function() { - var threw = false - try { - mount(null, createComponent({view: function() {}})) - } catch (e) { - threw = true - } - o(threw).equals(true) - }) - - o("renders into `root` synchronoulsy", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - o(root.firstChild.nodeName).equals("DIV") - }) - - o("mounting null unmounts", function() { - mount(root, createComponent({ - view : function() { - return m("div") - } - })) - - mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("Mounting a second root doesn't cause the first one to redraw", function() { - var view = o.spy(function() { - return m("div") - }) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : view - })) - - o(root.firstChild.nodeName).equals("DIV") - o(view.callCount).equals(1) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div") - } - })) - - o(view.callCount).equals(1) - - throttleMock.fire() - - o(view.callCount).equals(1) - }) - - o("redraws on events", function() { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view : function() { - return m("div", { - oninit : oninit, - onupdate : onupdate, - onclick : onclick, - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("redraws several mount points on events", function() { - var onupdate0 = o.spy() - var oninit0 = o.spy() - var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() - var onclick1 = o.spy() - - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], createComponent({ - view : function() { - return m("div", { - oninit : oninit0, - onupdate : onupdate0, - onclick : onclick0, - }) - } - })) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - mount(root.childNodes[1], createComponent({ - view : function() { - return m("div", { - oninit : oninit1, - onupdate : onupdate1, - onclick : onclick1, - }) - } - })) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root.childNodes[0].firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root.childNodes[0].firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root.childNodes[1].firstChild.dispatchEvent(e) - - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].firstChild) - - throttleMock.fire() - - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) - }) - - o("event handlers can skip redraw", function() { - var onupdate = o.spy(function(){ - throw new Error("This shouldn't have been called") - }) - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, createComponent({ - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false - } - }) - } - })) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - throttleMock.fire() - - o(onupdate.callCount).equals(0) - }) - - o("redraws when the render function is run", function() { - var onupdate = o.spy() - var oninit = o.spy() - - mount(root, createComponent({ - view : function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - })) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - redrawService.redraw() - - throttleMock.fire() - - o(onupdate.callCount).equals(1) - }) - - o("throttles", function() { - var i = 0 - mount(root, createComponent({view: function() {i++}})) - var before = i - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - var after = i - - throttleMock.fire() - - o(before).equals(1) // mounts synchronously - o(after).equals(1) // throttles rest - o(i).equals(2) - }) - }) - }) -}) diff --git a/api/tests/test-mountRedraw.js b/api/tests/test-mountRedraw.js new file mode 100644 index 000000000..7cb4432f6 --- /dev/null +++ b/api/tests/test-mountRedraw.js @@ -0,0 +1,413 @@ +"use strict" + +var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") +var domMock = require("../../test-utils/domMock") +var throttleMocker = require("../../test-utils/throttleMock") +var mountRedraw = require("../../api/mount-redraw") +var h = require("../../render/hyperscript") + +// Because Node doesn't have this. +if (typeof requestAnimationFrame !== "function") { + global.requestAnimationFrame = (function(delay, last) { + return function(callback) { + var elapsed = Date.now() - last + return setTimeout(function() { + callback() + last = Date.now() + }, delay - elapsed) + } + })(16, 0) +} + +o.spec("mount/redraw", function() { + var root, m, throttleMock, consoleMock, $document, errors + o.beforeEach(function() { + var $window = domMock() + consoleMock = {error: o.spy()} + throttleMock = throttleMocker() + root = $window.document.body + m = mountRedraw($window, throttleMock.schedule, consoleMock) + $document = $window.document + errors = [] + }) + + o.afterEach(function() { + o(consoleMock.error.calls.map(function(c) { + return c.args[0] + })).deepEquals(errors) + o(throttleMock.queueLength()).equals(0) + }) + + o("shouldn't error if there are no renderers", function() { + m.redraw() + throttleMock.fire() + }) + + o("schedules correctly", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.redraw() + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("should run a single renderer entry", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + + o(spy.callCount).equals(1) + + m.redraw() + m.redraw() + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("should run all renderer entries", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + m.mount(el1, {view: spy1}) + m.mount(el2, {view: spy2}) + m.mount(el3, {view: spy3}) + + m.redraw() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + m.redraw() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + throttleMock.fire() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + }) + + o("should stop running after mount null", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root, null) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should stop running after mount undefined", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root, undefined) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should stop running after mount no arg", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.mount(root) + + m.redraw() + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("should invoke remove callback on unmount", function() { + var spy = o.spy() + var onremove = o.spy() + + m.mount(root, {view: spy, onremove: onremove}) + o(spy.callCount).equals(1) + m.mount(root) + + o(spy.callCount).equals(1) + o(onremove.callCount).equals(1) + }) + + o("should stop running after unsubscribe, even if it occurs after redraw is requested", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + m.redraw() + m.mount(root) + + o(spy.callCount).equals(1) + throttleMock.fire() + o(spy.callCount).equals(1) + }) + + o("does nothing on invalid unmount", function() { + var spy = o.spy() + + m.mount(root, {view: spy}) + o(spy.callCount).equals(1) + + m.mount(null) + m.redraw() + throttleMock.fire() + o(spy.callCount).equals(2) + }) + + o("redraw.sync() redraws all roots synchronously", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + m.mount(el1, {view: spy1}) + m.mount(el2, {view: spy2}) + m.mount(el3, {view: spy3}) + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + m.redraw.sync() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + + m.redraw.sync() + + o(spy1.callCount).equals(3) + o(spy2.callCount).equals(3) + o(spy3.callCount).equals(3) + }) + + + o("throws on invalid component", function() { + o(function() { m.mount(root, {}) }).throws(TypeError) + }) + + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("throws on invalid `root` DOM node", function() { + o(function() { + m.mount(null, createComponent({view: function() {}})) + }).throws(TypeError) + }) + + o("renders into `root` synchronously", function() { + m.mount(root, createComponent({ + view: function() { + return h("div") + } + })) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("mounting null unmounts", function() { + m.mount(root, createComponent({ + view: function() { + return h("div") + } + })) + + m.mount(root, null) + + o(root.childNodes.length).equals(0) + }) + + o("Mounting a second root doesn't cause the first one to redraw", function() { + var root1 = $document.createElement("div") + var root2 = $document.createElement("div") + var view = o.spy() + + m.mount(root1, createComponent({view: view})) + o(view.callCount).equals(1) + + m.mount(root2, createComponent({view: function() {}})) + + o(view.callCount).equals(1) + + throttleMock.fire() + o(view.callCount).equals(1) + }) + + o("redraws on events", function() { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate, + onclick: onclick, + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + throttleMock.fire() + + o(onupdate.callCount).equals(1) + }) + + o("redraws several mount points on events", function() { + var onupdate0 = o.spy() + var oninit0 = o.spy() + var onclick0 = o.spy() + var onupdate1 = o.spy() + var oninit1 = o.spy() + var onclick1 = o.spy() + + var root1 = $document.createElement("div") + var root2 = $document.createElement("div") + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root1, createComponent({ + view: function() { + return h("div", { + oninit: oninit0, + onupdate: onupdate0, + onclick: onclick0, + }) + } + })) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + m.mount(root2, createComponent({ + view: function() { + return h("div", { + oninit: oninit1, + onupdate: onupdate1, + onclick: onclick1, + }) + } + })) + + o(oninit1.callCount).equals(1) + o(onupdate1.callCount).equals(0) + + root1.firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root1.firstChild) + + throttleMock.fire() + + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) + + root2.firstChild.dispatchEvent(e) + + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root2.firstChild) + + throttleMock.fire() + + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) + }) + + o("event handlers can skip redraw", function() { + var onupdate = o.spy(function(){ + throw new Error("This shouldn't have been called") + }) + var oninit = o.spy() + var e = $document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate, + onclick: function(e) { + e.redraw = false + } + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + throttleMock.fire() + + o(onupdate.callCount).equals(0) + }) + + o("redraws when the render function is run", function() { + var onupdate = o.spy() + var oninit = o.spy() + + m.mount(root, createComponent({ + view: function() { + return h("div", { + oninit: oninit, + onupdate: onupdate + }) + } + })) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + m.redraw() + + throttleMock.fire() + + o(onupdate.callCount).equals(1) + }) + }) + }) +}) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js deleted file mode 100644 index a04dbe847..000000000 --- a/api/tests/test-redraw.js +++ /dev/null @@ -1,195 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var domMock = require("../../test-utils/domMock") -var throttleMocker = require("../../test-utils/throttleMock") -var apiRedraw = require("../../api/redraw") - -// Because Node doesn't have this. -if (typeof requestAnimationFrame !== "function") { - global.requestAnimationFrame = (function (delay, last) { - return function(callback) { - var elapsed = Date.now() - last - return setTimeout(function() { - callback() - last = Date.now() - }, delay - elapsed) - } - })(16, 0) -} - -o.spec("redrawService", function() { - var root, redrawService, $document - o.beforeEach(function() { - var $window = domMock() - root = $window.document.body - redrawService = apiRedraw($window) - $document = $window.document - }) - - o("shouldn't error if there are no renderers", function() { - redrawService.redraw() - }) - - o("honours throttleMock", function() { - var throttleMock = throttleMocker() - redrawService = apiRedraw(domMock(), throttleMock.throttle) - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - - o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(2) - }) - - o("should run a single renderer entry", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - - o(spy.callCount).equals(1) - - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - o("should run all renderer entries", function(done) { - var el1 = $document.createElement("div") - var el2 = $document.createElement("div") - var el3 = $document.createElement("div") - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - redrawService.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - redrawService.redraw() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - setTimeout(function() { - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - - done() - }, 20) - }) - - o("should stop running after unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - redrawService.unsubscribe(root) - - redrawService.redraw() - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("should invoke remove callback on unsubscribe", function() { - var spy = o.spy() - var onremove = o.spy() - - redrawService.subscribe(root, spy, onremove) - o(spy.callCount).equals(1) - redrawService.unsubscribe(root) - - o(spy.callCount).equals(1) - o(onremove.callCount).equals(1) - }) - - o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - - redrawService.redraw() - - redrawService.unsubscribe(root) - - o(spy.callCount).equals(1) - setTimeout(function() { - o(spy.callCount).equals(1) - - done() - }, 20) - }) - - o("does nothing on invalid unsubscribe", function(done) { - var spy = o.spy() - - redrawService.subscribe(root, spy) - o(spy.callCount).equals(1) - - redrawService.unsubscribe(null) - redrawService.redraw() - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, 20) - }) - - o("redraw.sync() redraws all roots synchronously", function() { - var el1 = $document.createElement("div") - var el2 = $document.createElement("div") - var el3 = $document.createElement("div") - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - redrawService.subscribe(el1, spy1) - redrawService.subscribe(el2, spy2) - redrawService.subscribe(el3, spy3) - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - redrawService.redraw.sync() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - - redrawService.redraw.sync() - - o(spy1.callCount).equals(3) - o(spy2.callCount).equals(3) - o(spy3.callCount).equals(3) - }) -}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 5aed53bde..5ec418398 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -7,8 +7,7 @@ var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") -var apiMount = require("../../api/mount") -var apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -16,7 +15,7 @@ o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, redrawService, route, throttleMock + var $window, root, mountRedraw, route, throttleMock o.beforeEach(function() { $window = browserMock(env) @@ -24,8 +23,8 @@ o.spec("route", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService, apiMount(redrawService)) + mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) @@ -223,7 +222,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -243,7 +242,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -262,7 +261,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() o(view.callCount).equals(1) @@ -284,8 +283,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") - // unsubscribe as if via `m.mount(root)` - redrawService.unsubscribe(root) + mountRedraw.mount(root) o(root.childNodes.length).equals(0) }) @@ -352,7 +350,7 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(onupdate.callCount).equals(1) @@ -821,7 +819,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -857,7 +855,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(matchCount).equals(1) @@ -1182,7 +1180,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) - redrawService.redraw() + mountRedraw.redraw() throttleMock.fire() o(view.callCount).equals(2) @@ -1446,10 +1444,10 @@ o.spec("route", function() { }) var before = i - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() - redrawService.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() + mountRedraw.redraw() var after = i throttleMock.fire() diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 57ec0c406..d507b97b8 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -6,15 +6,14 @@ var browserMock = require("../../test-utils/browserMock") var throttleMocker = require("../../test-utils/throttleMock") var callAsync = require("../../test-utils/callAsync") -var apiMount = require("../../api/mount") -var apiRedraw = require("../../api/redraw") +var apiMountRedraw = require("../../api/mount-redraw") var apiRouter = require("../../api/router") o.spec("route.get/route.set", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var $window, root, redrawService, route, throttleMock + var $window, root, mountRedraw, route, throttleMock o.beforeEach(function() { $window = browserMock(env) @@ -22,8 +21,8 @@ o.spec("route.get/route.set", function() { root = $window.document.body - redrawService = apiRedraw($window, throttleMock.throttle) - route = apiRouter($window, redrawService, apiMount(redrawService)) + mountRedraw = apiMountRedraw($window, throttleMock.schedule, console) + route = apiRouter($window, mountRedraw) route.prefix(prefix) }) diff --git a/docs/index.md b/docs/index.md index 8589f517f..4fbffd826 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ It's small (< 10kb gzip), fast and provides routing and XHR utilities out of the
Download size
- Mithril (9.6kb) + Mithril (9.4kb)
Vue + Vue-Router + Vuex + fetch (40kb)
diff --git a/docs/route.md b/docs/route.md index 787ab0a83..06caa4ceb 100644 --- a/docs/route.md +++ b/docs/route.md @@ -739,7 +739,7 @@ m.route(document.body, "/", { In certain situations, you may find yourself needing to interoperate with another framework like React. Here's how you do it: - Define all your routes using `m.route` as normal, but make sure you only use it *once*. Multiple route points are not supported. -- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. +- When you need to remove routing subscriptions, use `m.mount(root, null)`, using the same root you used `m.route(root, ...)` on. `m.route` uses `m.mount` internally to hook everything up, so it's not magic. Here's an example with React: diff --git a/index.js b/index.js index 1f775a5dc..120ccb29a 100644 --- a/index.js +++ b/index.js @@ -7,14 +7,14 @@ m.trust = hyperscript.trust m.fragment = hyperscript.fragment var requestService = require("./request") -var redrawService = require("./redraw") +var mountRedraw = require("./mount-redraw") -requestService.setCompletionCallback(redrawService.redraw) +requestService.setCompletionCallback(mountRedraw.redraw) -m.mount = require("./mount") +m.mount = mountRedraw.mount m.route = require("./route") m.render = require("./render").render -m.redraw = redrawService.redraw +m.redraw = mountRedraw.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = require("./querystring/parse") diff --git a/mount-redraw.js b/mount-redraw.js new file mode 100644 index 000000000..bcdf52ef7 --- /dev/null +++ b/mount-redraw.js @@ -0,0 +1,3 @@ +"use strict" + +module.exports = require("./api/mount-redraw")(window, requestAnimationFrame, console) diff --git a/mount.js b/mount.js index 73fa1ce9d..6e169443f 100644 --- a/mount.js +++ b/mount.js @@ -1,5 +1,3 @@ "use strict" -var redrawService = require("./redraw") - -module.exports = require("./api/mount")(redrawService) +module.exports = require("./mount-redraw").mount diff --git a/redraw.js b/redraw.js index 314e5002a..af43394dc 100644 --- a/redraw.js +++ b/redraw.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./api/redraw")(window) +module.exports = require("./mount-redraw").redraw diff --git a/render/render.js b/render/render.js index c54ba654c..13fcd6ec2 100644 --- a/render/render.js +++ b/render/render.js @@ -884,7 +884,7 @@ module.exports = function($window) { } function render(dom, vnodes) { - if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") + if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = activeElement() var namespace = dom.namespaceURI diff --git a/route.js b/route.js index a29cd9f67..9cf121e23 100644 --- a/route.js +++ b/route.js @@ -1,6 +1,5 @@ "use strict" -var redrawService = require("./redraw") -var mount = require("./mount") +var mountRedraw = require("./mount-redraw") -module.exports = require("./api/router")(window, redrawService, mount) +module.exports = require("./api/router")(window, mountRedraw) diff --git a/test-utils/tests/test-throttleMock.js b/test-utils/tests/test-throttleMock.js index 699206236..fd00d2bd0 100644 --- a/test-utils/tests/test-throttleMock.js +++ b/test-utils/tests/test-throttleMock.js @@ -4,88 +4,35 @@ var o = require("../../ospec/ospec") var throttleMocker = require("../../test-utils/throttleMock") o.spec("throttleMock", function() { - o("works with one callback", function() { + o("schedules one callback", function() { var throttleMock = throttleMocker() var spy = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled = throttleMock.throttle(spy) - - o(throttleMock.queueLength()).equals(0) - o(spy.callCount).equals(0) - - throttled() - + throttleMock.schedule(spy) o(throttleMock.queueLength()).equals(1) o(spy.callCount).equals(0) - - throttled() - - o(throttleMock.queueLength()).equals(1) - o(spy.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy.callCount).equals(1) - - throttleMock.fire() - - o(spy.callCount).equals(1) }) - o("works with two callbacks", function() { + o("schedules two callbacks", function() { var throttleMock = throttleMocker() var spy1 = o.spy() var spy2 = o.spy() o(throttleMock.queueLength()).equals(0) - - var throttled1 = throttleMock.throttle(spy1) - - o(throttleMock.queueLength()).equals(0) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled1() - + throttleMock.schedule(spy1) o(throttleMock.queueLength()).equals(1) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled1() - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - var throttled2 = throttleMock.throttle(spy2) - - o(throttleMock.queueLength()).equals(1) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - - throttled2() - + throttleMock.schedule(spy2) o(throttleMock.queueLength()).equals(2) o(spy1.callCount).equals(0) o(spy2.callCount).equals(0) - - throttled2() - - o(throttleMock.queueLength()).equals(2) - o(spy1.callCount).equals(0) - o(spy2.callCount).equals(0) - throttleMock.fire() - o(throttleMock.queueLength()).equals(0) o(spy1.callCount).equals(1) o(spy2.callCount).equals(1) - - throttleMock.fire() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) }) }) diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js index 6cdb57103..21eb53be6 100644 --- a/test-utils/throttleMock.js +++ b/test-utils/throttleMock.js @@ -3,17 +3,8 @@ module.exports = function() { var queue = [] return { - throttle: function(fn) { - var pending = false - return function() { - if (!pending) { - queue.push(function(){ - pending = false - fn() - }) - pending = true - } - } + schedule: function(fn) { + queue.push(fn) }, fire: function() { var tasks = queue