diff --git a/README.md b/README.md index e1be8c2b..71c22280 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ There are over 4000 assertions in the test suite, and tests cover even difficult ## Modularity -Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.42 KB min+gzip +Despite the huge improvements in performance and modularity, the new codebase is smaller than v0.2.x, currently clocking at 7.41 KB min+gzip In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/api/autoredraw.js b/api/autoredraw.js deleted file mode 100644 index f3b88c03..00000000 --- a/api/autoredraw.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict" - -var throttle = require("../api/throttle") - -module.exports = function(root, renderer, pubsub, callback) { - var run = throttle(callback) - if (renderer != null) { - renderer.setEventCallback(function(e) { - if (e.redraw !== false) pubsub.publish() - }) - } - - if (pubsub != null) { - if (root.redraw) pubsub.unsubscribe(root.redraw) - pubsub.subscribe(run) - } - - return root.redraw = run -} diff --git a/api/mount.js b/api/mount.js index 9ff6c81b..2afadc57 100644 --- a/api/mount.js +++ b/api/mount.js @@ -1,23 +1,42 @@ "use strict" var Vnode = require("../render/vnode") -var autoredraw = require("../api/autoredraw") -module.exports = function(renderer, pubsub) { +module.exports = function(redrawService) { + function throttle(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, time - (now - last)) + } + } + } + return function(root, component) { if (component === null) { - renderer.render(root, []) - pubsub.unsubscribe(root.redraw) - delete root.redraw + redrawService.render(root, []) + redrawService.unsubscribe(root) return } if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - - var run = autoredraw(root, renderer, pubsub, function() { - renderer.render(root, Vnode(component, undefined, undefined, undefined, undefined, undefined)) + + var run = throttle(function() { + redrawService.render(root, Vnode(component)) }) - + redrawService.subscribe(root, run) run() } } diff --git a/api/pubsub.js b/api/pubsub.js deleted file mode 100644 index 7d9fbb57..00000000 --- a/api/pubsub.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -module.exports = function() { - var callbacks = [] - function unsubscribe(callback) { - var index = callbacks.indexOf(callback) - if (index > -1) callbacks.splice(index, 1) - } - function publish() { - for (var i = 0; i < callbacks.length; i++) { - callbacks[i].apply(this, arguments) - } - } - return {subscribe: callbacks.push.bind(callbacks), unsubscribe: unsubscribe, publish: publish} -} diff --git a/api/redraw.js b/api/redraw.js new file mode 100644 index 00000000..d2d20c52 --- /dev/null +++ b/api/redraw.js @@ -0,0 +1,26 @@ +"use strict" + +var coreRenderer = require("../render/render") + +module.exports = function($window) { + var renderService = coreRenderer($window) + renderService.setEventCallback(function(e) { + if (e.redraw !== false) redraw() + }) + + var callbacks = [] + function subscribe(key, callback) { + unsubscribe(key) + callbacks.push(key, callback) + } + function unsubscribe(key) { + var index = callbacks.indexOf(key) + if (index > -1) callbacks.splice(index, 2) + } + function redraw() { + for (var i = 1; i < callbacks.length; i += 2) { + callbacks[i]() + } + } + return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} +} diff --git a/api/router.js b/api/router.js index 459f68c8..553a6751 100644 --- a/api/router.js +++ b/api/router.js @@ -3,55 +3,44 @@ var Vnode = require("../render/vnode") var coreRouter = require("../router/router") -module.exports = function($window, mount) { - var router = coreRouter($window) - var currentResolve, currentComponent, currentRender, currentArgs, currentPath - - var RouteComponent = {view: function() { - return [currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined))] - }} - function defaultRender(vnode) { - return vnode - } +module.exports = function($window, redrawService) { + var routeService = coreRouter($window) + + var identity = function(v) {return v} + var current = {render: identity, component: null, path: null, resolve: null} var route = function(root, defaultRoute, routes) { - currentComponent = "div" - currentRender = defaultRender - currentArgs = null - - mount(root, RouteComponent) - - router.defineRoutes(routes, function(payload, args, path) { - var isResolver = typeof payload.view !== "function" - var render = defaultRender - - var resolve = currentResolve = function (component) { - if (resolve !== currentResolve) return - currentResolve = null - - currentComponent = component != null ? component : isResolver ? "div" : payload - currentRender = render - currentArgs = args - currentPath = path - - root.redraw(true) + if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") + var render = function(resolver, component, params, path) { + current.render = resolver.render || identity + current.component = component + current.path = path + current.resolve = null + redrawService.render(root, current.render(Vnode(component, undefined, params))) + } + var run = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + if (component.view) render({}, component, params, path) + else { + if (component.onmatch) { + if (isRouteChange === false && current.path === path || current.resolve != null) render(current, current.component, params) + else { + current.resolve = function(resolved) { + render(component, resolved, params, path) + } + component.onmatch(function(resolved) { + if (current.path !== path && current.resolve != null) current.resolve(resolved) + }, params, path) + } + } + else render(component, "div", params, path) } - var onmatch = function() { - resolve() - } - if (isResolver) { - if (typeof payload.render === "function") render = payload.render.bind(payload) - if (typeof payload.onmatch === "function") onmatch = payload.onmatch - } - - onmatch.call(payload, resolve, args, path) }, function() { - router.setPath(defaultRoute, null, {replace: true}) + routeService.setPath(defaultRoute) }) + redrawService.subscribe(root, run) } - route.link = router.link - route.prefix = router.setPrefix - route.set = router.setPath - route.get = function() {return currentPath} - + route.set = routeService.setPath + route.get = function() {return current.path} + route.prefix = routeService.setPrefix + route.link = routeService.link return route } diff --git a/api/tests/index.html b/api/tests/index.html index 75d15bea..fd2557d5 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -14,7 +14,7 @@ - + @@ -24,15 +24,11 @@ - - - + - - - + diff --git a/api/tests/test-autoredraw.js b/api/tests/test-autoredraw.js deleted file mode 100644 index 191d2815..00000000 --- a/api/tests/test-autoredraw.js +++ /dev/null @@ -1,95 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var domMock = require("../../test-utils/domMock") - -var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") -var autoredraw = require("../../api/autoredraw") - -o.spec("autoredraw", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, renderer, pubsub, spy - o.beforeEach(function() { - $window = domMock() - root = $window.document.body - renderer = coreRenderer($window) - pubsub = apiPubSub() - spy = o.spy() - }) - - o("returns self-trigger", function() { - var run = autoredraw(root, renderer, pubsub, spy) - - run() - - o(spy.callCount).equals(1) - }) - - o("null renderer doesn't throw", function(done) { - autoredraw(root, null, pubsub, spy) - done() - }) - - o("null pubsub doesn't throw", function(done) { - autoredraw(root, renderer, null, spy) - done() - }) - - o("registers onevent", function() { - autoredraw(root, renderer, pubsub, spy) - - renderer.render(root, {tag: "div", attrs: {onclick: function() {}}}) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - o(spy.callCount).equals(1) - }) - - o("registers pubsub", function() { - autoredraw(root, renderer, pubsub, spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) - - o("re-registering pubsub works", function() { - autoredraw(root, renderer, pubsub, spy) - autoredraw(root, renderer, pubsub, spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) - - o("throttles", function(done) { - var run = autoredraw(root, renderer, pubsub, spy) - - run() - run() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }) - - o("does not redraw if e.redraw is false", function() { - autoredraw(root, renderer, pubsub, spy) - - renderer.render(root, {tag: "div", attrs: {onclick: function(e) {e.redraw = false}}}) - - var e = $window.document.createEvent("MouseEvents") - e.initEvent("click", true, true) - root.firstChild.dispatchEvent(e) - - o(spy.callCount).equals(0) - }) - -}) diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index b65daf4a..f3b1b431 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -5,20 +5,20 @@ var domMock = require("../../test-utils/domMock") var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") +var apiRedraw = require("../../api/redraw") var apiMounter = require("../../api/mount") o.spec("mount", function() { var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redraw, mount, render + var $window, root, redrawService, mount, render o.beforeEach(function() { $window = domMock() root = $window.document.body - redraw = apiPubSub() - mount = apiMounter(coreRenderer($window), redraw) + redrawService = apiRedraw($window) + mount = apiMounter(redrawService) render = coreRenderer($window).render }) @@ -42,18 +42,16 @@ o.spec("mount", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("mounting null deletes `redraw` from `root`", function() { + o("mounting null unmounts", function() { mount(root, { view : function() { return m("div") } }) - o(typeof root.redraw).equals('function') - mount(root, null) - o(typeof root.redraw).equals('undefined') + o(root.childNodes.length).equals(0) }) o("redraws on events", function(done) { @@ -206,7 +204,7 @@ o.spec("mount", function() { o(oninit.callCount).equals(1) o(onupdate.callCount).equals(0) - redraw.publish() + redrawService.redraw() // Wrapped to give time for the rate-limited redraw to fire setTimeout(function() { diff --git a/api/tests/test-pubsub.js b/api/tests/test-pubsub.js deleted file mode 100644 index 270b7c0e..00000000 --- a/api/tests/test-pubsub.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var apiPubSub = require("../../api/pubsub") - -o.spec("pubsub", function() { - var pubsub - o.beforeEach(function() { - pubsub = apiPubSub() - }) - - o("shouldn't error if there are no renderers", function() { - pubsub.publish() - }) - - o("should run a single renderer entry", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - - pubsub.publish() - - o(spy.callCount).equals(1) - - pubsub.publish() - pubsub.publish() - pubsub.publish() - - o(spy.callCount).equals(4) - }) - - o("should run all renderer entries", function() { - var spy1 = o.spy() - var spy2 = o.spy() - var spy3 = o.spy() - - pubsub.subscribe(spy1) - pubsub.subscribe(spy2) - pubsub.subscribe(spy3) - - pubsub.publish() - - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) - - pubsub.publish() - - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - }) - - o("should stop running after unsubscribe", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - pubsub.unsubscribe(spy) - - pubsub.publish() - - o(spy.callCount).equals(0) - }) - - o("does nothing on invalid unsubscribe", function() { - var spy = o.spy() - - pubsub.subscribe(spy) - pubsub.unsubscribe(null) - - pubsub.publish() - - o(spy.callCount).equals(1) - }) -}) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js new file mode 100644 index 00000000..5f002e5f --- /dev/null +++ b/api/tests/test-redraw.js @@ -0,0 +1,82 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var apiRedraw = require("../../api/redraw") + +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("should run a single renderer entry", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + + redrawService.redraw() + + o(spy.callCount).equals(1) + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + + o(spy.callCount).equals(4) + }) + + 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() + + 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(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + }) + + o("should stop running after unsubscribe", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + redrawService.unsubscribe(root, spy) + + redrawService.redraw() + + o(spy.callCount).equals(0) + }) + + o("does nothing on invalid unsubscribe", function() { + var spy = o.spy() + + redrawService.subscribe(root, spy) + redrawService.unsubscribe(null) + + redrawService.redraw() + + o(spy.callCount).equals(1) + }) +}) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 721d6b28..0bd394dc 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -6,25 +6,23 @@ var browserMock = require("../../test-utils/browserMock") var m = require("../../render/hyperscript") var coreRenderer = require("../../render/render") -var apiPubSub = require("../../api/pubsub") +var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") -var apiMounter = require("../../api/mount") 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 FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redraw, mount, route + var $window, root, redrawService, route o.beforeEach(function() { $window = browserMock(env) root = $window.document.body - redraw = apiPubSub() - mount = apiMounter(coreRenderer($window), redraw) - route = apiRouter($window, mount) + redrawService = apiRedraw($window) + route = apiRouter($window, redrawService) route.prefix(prefix) }) @@ -59,7 +57,7 @@ o.spec("route", function() { o(view.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(view.callCount).equals(2) @@ -122,15 +120,15 @@ o.spec("route", function() { o(oninit.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(onupdate.callCount).equals(1) }) o("redraws on events", function(done) { var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() + var oninit = o.spy() + var onclick = o.spy() var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -403,7 +401,7 @@ o.spec("route", function() { o(matchCount).equals(1) o(renderCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(matchCount).equals(1) o(renderCount).equals(2) @@ -505,7 +503,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(onmatch.callCount).equals(1) - redraw.publish(true) + redrawService.redraw() o(view.callCount).equals(2) o(onmatch.callCount).equals(1) @@ -515,7 +513,7 @@ o.spec("route", function() { }) o("m.route.set(m.route.get()) re-runs the resolution logic (#1180)", function(done){ - var onmatch = o.spy(function(resolve){resolve()}) + var onmatch = o.spy(function(resolve) {resolve()}) $window.location.href = prefix + "/" route(root, '/', { diff --git a/api/tests/test-throttle.js b/api/tests/test-throttle.js deleted file mode 100644 index 4f7c7ee5..00000000 --- a/api/tests/test-throttle.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict" - -var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") -var throttle = require("../../api/throttle") - -o.spec("throttle", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var spy, throttled - o.beforeEach(function() { - spy = o.spy() - throttled = throttle(spy) - }) - - o("runs first call synchronously", function() { - throttled() - - o(spy.callCount).equals(1) - }) - - o("throttles subsequent synchronous calls", function(done) { - throttled() - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) //this delay is much higher than 16.6ms due to setTimeout clamp and other runtime costs - }) - - o("calls after threshold", function(done) { - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function(t) { - throttled() - - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - - }) - - o("throttles before threshold", function(done) { - throttled() - - o(spy.callCount).equals(1) - - callAsync(function(t) { - throttled() - - o(spy.callCount).equals(1) - - done() - }) - }) - - o("it only runs once per tick", function(done) { - throttled() - throttled() - throttled() - - o(spy.callCount).equals(1) - - setTimeout(function() { - o(spy.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }) - - o("it supports forcing a synchronous redraw", function(done) { - throttled() - throttled() - throttled(true) - - o(spy.callCount).equals(2) - - setTimeout(function() { - o(spy.callCount).equals(3) - - done() - }, FRAME_BUDGET) - }) -}) diff --git a/api/throttle.js b/api/throttle.js deleted file mode 100644 index 6655b07c..00000000 --- a/api/throttle.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict" - -module.exports = function(callback) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function(synchronous) { - var now = Date.now() - if (synchronous === true || last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback() - last = Date.now() - }, time - (now - last)) - } - } -} diff --git a/bundler/bundle.js b/bundler/bundle.js index dff20bfe..4ac98300 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -32,8 +32,9 @@ function run(input, output) { def = def || "", variable = variable || "", eq = eq || "", rest = rest || "" if (def[0] === ",") def = "\nvar ", pre = "\n" var dependency = resolve(filepath, filename) - var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, uuid) : def + variable + eq + modules[dependency])) - modules[dependency] = rest ? "_" + uuid : variable + var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption + var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency])) + modules[dependency] = rest ? "_" + localUUID : variable uuid++ return code + rest }) @@ -116,7 +117,7 @@ function run(input, output) { code = "new function() {\n" + code + "\n}" if (!isFile(output) || code !== read(output)) { - try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} + //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} error = null fs.writeFileSync(output, code, "utf8") } @@ -129,7 +130,7 @@ function run(input, output) { module.exports = function(input, output, options) { run(input, output) if (options && options.watch) { - fs.watch(process.cwd(), {recursive: true}, function(file) { + fs.watch(process.cwd(), {recursive: true}, function(file, type) { if (typeof file === "string" && path.resolve(output) !== path.resolve(file)) run(input, output) }) } diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 80763c00..60859a11 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -273,6 +273,30 @@ o.spec("bundler", function() { remove("d.js") remove("out.js") }) + o("works if included multiple times", function() { + write("a.js", `module.exports = 123`) + write("b.js", `var a = require("./a").toString()\nmodule.exports = a`) + write("c.js", `var a = require("./a").toString()\nvar b = require("./b")`) + bundle(ns + "c.js", ns + "out.js") + + o(read("out.js")).equals(`new function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}`) + + remove("a.js") + remove("b.js") + remove("c.js") + }) + o("works if included multiple times reverse", function() { + write("a.js", `module.exports = 123`) + write("b.js", `var a = require("./a").toString()\nmodule.exports = a`) + write("c.js", `var b = require("./b")\nvar a = require("./a").toString()`) + bundle(ns + "c.js", ns + "out.js") + + o(read("out.js")).equals(`new function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}`) + + remove("a.js") + remove("b.js") + remove("c.js") + }) o("reuses binding if possible", function() { write("a.js", `var b = require("./b")\nvar c = require("./c")`) write("b.js", `var d = require("./d")\nmodule.exports = function() {return d + 1}`) diff --git a/index.js b/index.js index 8ae9df25..f50049c4 100644 --- a/index.js +++ b/index.js @@ -4,13 +4,13 @@ var m = require("./hyperscript") var requestService = require("./request") var redrawService = require("./redraw") -requestService.setCompletionCallback(redrawService.publish) +requestService.setCompletionCallback(redrawService.redraw) m.mount = require("./mount") m.route = require("./route") m.withAttr = require("./util/withAttr") -m.render = require("./render").render -m.redraw = redrawService.publish +m.render = redrawService.render +m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = require("./querystring/parse") diff --git a/mithril.js b/mithril.js index 7fdbec3a..4c29abab 100644 --- a/mithril.js +++ b/mithril.js @@ -328,22 +328,7 @@ var _8 = function($window, Promise) { return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback} } var requestService = _8(window, PromisePolyfill) -var _11 = function() { - var callbacks = [] - function unsubscribe(callback) { - var index = callbacks.indexOf(callback) - if (index > -1) callbacks.splice(index, 1) - } - function publish() { - for (var i = 0; i < callbacks.length; i++) { - callbacks[i].apply(this, arguments) - } - } - return {subscribe: callbacks.push.bind(callbacks), unsubscribe: unsubscribe, publish: publish} -} -var redrawService = _11() -requestService.setCompletionCallback(redrawService.publish) -var _13 = function($window) { +var coreRenderer = function($window) { var $doc = $window.document var $emptyFragment = $doc.createDocumentFragment() var onevent @@ -642,8 +627,8 @@ var _13 = function($window) { for (var i = 0; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - var key1 = vnode.key - if (key1 != null) map[key1] = i + var key2 = vnode.key + if (key2 != null) map[key2] = i } } return map @@ -749,34 +734,34 @@ var _13 = function($window) { } //attrs2 function setAttrs(vnode, attrs2, ns) { - for (var key1 in attrs2) { - setAttr(vnode, key1, null, attrs2[key1], ns) + for (var key2 in attrs2) { + setAttr(vnode, key2, null, attrs2[key2], ns) } } - function setAttr(vnode, key1, old, value, ns) { + function setAttr(vnode, key2, old, value, ns) { var element = vnode.dom - if (key1 === "key" || (old === value && !isFormAttribute(vnode, key1)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key1)) return - var nsLastIndex = key1.indexOf(":") - if (nsLastIndex > -1 && key1.substr(0, nsLastIndex) === "xlink") { - element.setAttributeNS("http://www.w3.org/1999/xlink", key1.slice(nsLastIndex + 1), value) + if (key2 === "key" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return + var nsLastIndex = key2.indexOf(":") + if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") { + element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value) } - else if (key1[0] === "o" && key1[1] === "n" && typeof value === "function") updateEvent(vnode, key1, value) - else if (key1 === "style") updateStyle(element, old, value) - else if (key1 in element && !isAttribute(key1) && ns === undefined) { + else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value) + else if (key2 === "style") updateStyle(element, old, value) + else if (key2 in element && !isAttribute(key2) && ns === undefined) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key1 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "input" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key1 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "select" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key1 === "value" && vnode.dom.value === value) return - element[key1] = value + if (vnode.tag === "option" && key2 === "value" && vnode.dom.value === value) return + element[key2] = value } else { if (typeof value === "boolean") { - if (value) element.setAttribute(key1, "") - else element.removeAttribute(key1) + if (value) element.setAttribute(key2, "") + else element.removeAttribute(key2) } - else element.setAttribute(key1 === "className" ? "class" : key1, value) + else element.setAttribute(key2 === "className" ? "class" : key2, value) } } function setLateAttrs(vnode) { @@ -788,16 +773,16 @@ var _13 = function($window) { } function updateAttrs(vnode, old, attrs2, ns) { if (attrs2 != null) { - for (var key1 in attrs2) { - setAttr(vnode, key1, old && old[key1], attrs2[key1], ns) + for (var key2 in attrs2) { + setAttr(vnode, key2, old && old[key2], attrs2[key2], ns) } } if (old != null) { - for (var key1 in old) { - if (attrs2 == null || !(key1 in attrs2)) { - if (key1 === "className") key1 = "class" - if (key1[0] === "o" && key1[1] === "n" && !isLifecycleMethod(key1)) updateEvent(vnode, key1, undefined) - else if (key1 !== "key") vnode.dom.removeAttribute(key1) + for (var key2 in old) { + if (attrs2 == null || !(key2 in attrs2)) { + if (key2 === "className") key2 = "class" + if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined) + else if (key2 !== "key") vnode.dom.removeAttribute(key2) } } } @@ -821,32 +806,32 @@ var _13 = function($window) { else if (typeof style === "string") element.style.cssText = style else { if (typeof old === "string") element.style.cssText = "" - for (var key1 in style) { - element.style[key1] = style[key1] + for (var key2 in style) { + element.style[key2] = style[key2] } if (old != null && typeof old !== "string") { - for (var key1 in old) { - if (!(key1 in style)) element.style[key1] = "" + for (var key2 in old) { + if (!(key2 in style)) element.style[key2] = "" } } } } //event - function updateEvent(vnode, key1, value) { + function updateEvent(vnode, key2, value) { var element = vnode.dom var callback = function(e) { var result = value.call(element, e) if (typeof onevent === "function") onevent.call(element, e) return result } - if (key1 in element) element[key1] = typeof value === "function" ? callback : null + if (key2 in element) element[key2] = typeof value === "function" ? callback : null else { - var eventName = key1.slice(2) + var eventName = key2.slice(2) if (vnode.events === undefined) vnode.events = {} - if (vnode.events[key1] != null) element.removeEventListener(eventName, vnode.events[key1], false) + if (vnode.events[key2] != null) element.removeEventListener(eventName, vnode.events[key2], false) if (typeof value === "function") { - vnode.events[key1] = callback - element.addEventListener(eventName, vnode.events[key1], false) + vnode.events[key2] = callback + element.addEventListener(eventName, vnode.events[key2], false) } } } @@ -888,79 +873,90 @@ var _13 = function($window) { } return {render: render, setEventCallback: setEventCallback} } -var renderService = _13(window) -var throttle = function(callback1) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function(synchronous) { - var now = Date.now() - if (synchronous === true || last === 0 || now - last >= time) { - last = now - callback1() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback1() - last = Date.now() - }, time - (now - last)) +var _11 = function($window) { + var renderService = coreRenderer($window) + renderService.setEventCallback(function(e) { + if (e.redraw !== false) redraw() + }) + + var callbacks = [] + function subscribe(key1, callback) { + unsubscribe(key1) + callbacks.push(key1, callback) + } + function unsubscribe(key1) { + var index = callbacks.indexOf(key1) + if (index > -1) callbacks.splice(index, 2) + } + function redraw() { + for (var i = 1; i < callbacks.length; i += 2) { + callbacks[i]() + } + } + return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} +} +var redrawService = _11(window) +requestService.setCompletionCallback(redrawService.redraw) +var _16 = function(redrawService0) { + function throttle(callback0) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback0() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback0() + last = Date.now() + }, time - (now - last)) + } } } -} -var autoredraw = function(root, renderer, pubsub, callback0) { - var run1 = throttle(callback0) - if (renderer != null) { - renderer.setEventCallback(function(e) { - if (e.redraw !== false) pubsub.publish() - }) - } - if (pubsub != null) { - if (root.redraw) pubsub.unsubscribe(root.redraw) - pubsub.subscribe(run1) - } - return root.redraw = run1 -} -var _17 = function(renderer, pubsub) { + return function(root, component) { if (component === null) { - renderer.render(root, []) - pubsub.unsubscribe(root.redraw) - delete root.redraw + redrawService0.render(root, []) + redrawService0.unsubscribe(root) return } if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - var run0 = autoredraw(root, renderer, pubsub, function() { - renderer.render(root, Vnode(component, undefined, undefined, undefined, undefined, undefined)) + + var run0 = throttle(function() { + redrawService0.render(root, Vnode(component)) }) + redrawService0.subscribe(root, run0) run0() } } -m.mount = _17(renderService, redrawService) -var mount = m.mount +m.mount = _16(redrawService) var parseQueryString = function(string) { if (string === "" || string == null) return {} if (string.charAt(0) === "?") string = string.slice(1) var entries = string.split("&"), data0 = {}, counters = {} for (var i = 0; i < entries.length; i++) { var entry = entries[i].split("=") - var key3 = decodeURIComponent(entry[0]) + var key4 = decodeURIComponent(entry[0]) var value = entry.length === 2 ? decodeURIComponent(entry[1]) : "" if (value === "true") value = true else if (value === "false") value = false - var levels = key3.split(/\]\[?|\[/) + var levels = key4.split(/\]\[?|\[/) var cursor = data0 - if (key3.indexOf("[") > -1) levels.pop() + if (key4.indexOf("[") > -1) levels.pop() for (var j = 0; j < levels.length; j++) { var level = levels[j], nextLevel = levels[j + 1] var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) var isValue = j === levels.length - 1 if (level === "") { - var key3 = levels.slice(0, j).join() - if (counters[key3] == null) counters[key3] = 0 - level = counters[key3]++ + var key4 = levels.slice(0, j).join() + if (counters[key4] == null) counters[key4] = 0 + level = counters[key4]++ } if (cursor[level] == null) { cursor[level] = isValue ? value : isNumber ? [] : {} @@ -982,11 +978,11 @@ var coreRouter = function($window) { } var asyncId function debounceAsync(f) { - return function() { + return function(e) { if (asyncId != null) return asyncId = callAsync0(function() { asyncId = null - f() + f(e) }) } } @@ -997,11 +993,11 @@ var coreRouter = function($window) { if (queryIndex > -1) { var queryEnd = hashIndex > -1 ? hashIndex : path.length var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd)) - for (var key2 in queryParams) queryData[key2] = queryParams[key2] + for (var key3 in queryParams) queryData[key3] = queryParams[key3] } if (hashIndex > -1) { var hashParams = parseQueryString(path.slice(hashIndex + 1)) - for (var key2 in hashParams) hashData[key2] = hashParams[key2] + for (var key3 in hashParams) hashData[key3] = hashParams[key3] } return path.slice(0, pathEnd) } @@ -1017,7 +1013,7 @@ var coreRouter = function($window) { var queryData = {}, hashData = {} path = parsePath(path, queryData, hashData) if (data != null) { - for (var key2 in data) queryData[key2] = data[key2] + for (var key3 in data) queryData[key3] = data[key3] path = path.replace(/:([^\/]+)/g, function(match2, token) { delete queryData[token] return data[token] @@ -1030,16 +1026,16 @@ var coreRouter = function($window) { if (supportsPushState) { if (options && options.replace) $window.history.replaceState(null, null, prefix1 + path) else $window.history.pushState(null, null, prefix1 + path) - $window.onpopstate() + $window.onpopstate(true) } else $window.location.href = prefix1 + path } - function defineRoutes(routes, resolve0, reject) { + function defineRoutes(routes, resolve, reject) { if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) else if (prefix1.charAt(0) === "#") $window.onhashchange = resolveRoute - resolveRoute() + resolveRoute(true) - function resolveRoute() { + function resolveRoute(isRouteChange) { var path = getPath() var params = {} var pathname = parsePath(path, params, params) @@ -1053,14 +1049,14 @@ var coreRouter = function($window) { for (var i = 0; i < keys.length; i++) { params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) } - resolve0(routes[route0], params, path, route0) + resolve(routes[route0], params, path, route0, !!isRouteChange) }) return } } reject(path, params) } - return resolveRoute + return function() {resolveRoute(false)} } function link(vnode2) { vnode2.dom.setAttribute("href", prefix1 + vnode2.attrs.href) @@ -1075,64 +1071,60 @@ var coreRouter = function($window) { } return {setPrefix: setPrefix, getPath: getPath, setPath: setPath, defineRoutes: defineRoutes, link: link} } -var _23 = function($window, mount0) { - var router = coreRouter($window) - var currentResolve, currentComponent, currentRender, currentArgs, currentPath - var RouteComponent = {view: function() { - return [currentRender(Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined))] - }} - function defaultRender(vnode1) { - return vnode1 - } +var _20 = function($window, redrawService0) { + var routeService = coreRouter($window) + + var identity = function(v) {return v} + var current = {render: identity, component: null, path: null, resolve: null} var route = function(root, defaultRoute, routes) { - currentComponent = "div" - currentRender = defaultRender - currentArgs = null - mount0(root, RouteComponent) - router.defineRoutes(routes, function(payload, args0, path) { - var isResolver = typeof payload.view !== "function" - var render1 = defaultRender - var resolve = currentResolve = function (component) { - if (resolve !== currentResolve) return - currentResolve = null - currentComponent = component != null ? component : isResolver ? "div" : payload - currentRender = render1 - currentArgs = args0 - currentPath = path - root.redraw(true) + if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") + var render1 = function(resolver, component, params, path) { + current.render = resolver.render || identity + current.component = component + current.path = path + current.resolve = null + redrawService0.render(root, current.render(Vnode(component, undefined, params))) + } + var run1 = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + if (component.view) render1({}, component, params, path) + else { + if (component.onmatch) { + if (isRouteChange === false && current.path === path || current.resolve != null) render1(current, current.component, params) + else { + current.resolve = function(resolved) { + render1(component, resolved, params, path) + } + component.onmatch(function(resolved) { + if (current.path !== path && current.resolve != null) current.resolve(resolved) + }, params, path) + } + } + else render1(component, "div", params, path) } - var onmatch = function() { - resolve() - } - if (isResolver) { - if (typeof payload.render === "function") render1 = payload.render.bind(payload) - if (typeof payload.onmatch === "function") onmatch = payload.onmatch - } - - onmatch.call(payload, resolve, args0, path) }, function() { - router.setPath(defaultRoute, null, {replace: true}) + routeService.setPath(defaultRoute) }) + redrawService0.subscribe(root, run1) } - route.link = router.link - route.prefix = router.setPrefix - route.set = router.setPath - route.get = function() {return currentPath} + route.set = routeService.setPath + route.get = function() {return current.path} + route.prefix = routeService.setPrefix + route.link = routeService.link return route } -m.route = _23(window, mount) -m.withAttr = function(attrName, callback2, context) { +m.route = _20(window, redrawService) +m.withAttr = function(attrName, callback1, context) { return function(e) { - return callback2.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } -m.render = renderService.render -m.redraw = redrawService.publish +m.render = redrawService.render +m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0-rc.5" +m.version = "1.0.0-rc.6" if (typeof module !== "undefined") module["exports"] = m else window.m = m } \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 41590c4a..3e9ac712 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,40 +1,40 @@ -new function(){function m(a,b,k,e,l,h){return{tag:a,key:b,attrs:k,children:e,text:l,dom:h,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function t(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===H[a]){for(var b,k,e=[],l={};b=O.exec(a);){var h=b[1],v=b[2];""===h&&""!==v?k=v:"#"===h?l.id=v:"."===h?e.push(v):"["===b[3][0]&&((h=b[6])&&(h=h.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===b[4]?e.push(h):l[b[4]]=h||!0)}0a.indexOf("?")?"?":"&";a+=e+f}return a}function v(a){try{return""!== -a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function r(a,b){if("function"===typeof a)if(b instanceof Array)for(var e=0;en.status||304===n.status)b(r(f.type,a));else{var h=Error(n.responseText),k;for(k in a)h[k]=a[k];e(h)}}catch(G){e(G)}}; -u&&null!=f.data?n.send(f.data):n.send()}))},jsonp:function(f){return e(new b(function(b,e){var n=f.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+m++,k=a.document.createElement("script");a[n]=function(e){k.parentNode.removeChild(k);b(r(f.type,e));delete a[n]};k.onerror=function(){k.parentNode.removeChild(k);e(Error("JSONP request failed"));delete a[n]};null==f.data&&(f.data={});f.url=l(f.url,f.data);f.data[f.callbackKey||"callback"]=n;k.src=h(f.url,f.data);a.document.documentElement.appendChild(k)}))}, -setCompletionCallback:function(a){q=a}}}(window,"undefined"!==typeof Promise?Promise:y),J=function(){var a=[];return{subscribe:a.push.bind(a),unsubscribe:function(b){b=a.indexOf(b);-1=A&&B>=l;){var x=a[A],m=d[l];if(x!==m||g)if(null==x)A++;else if(null==m)l++;else if(x.key===m.key)A++,l++,h(c,x,m,f,p(a,A,e),g,n),g&&x.tag===m.tag&&r(c,v(x),e); -else if(x=a[q],x!==m||g)if(null==x)q--;else if(null==m)l++;else if(x.key===m.key)h(c,x,m,f,p(a,q+1,e),g,n),(g||l=A&&B>=l;){x=a[q];m=d[B];if(x!==m||g)if(null==x)q--;else{if(null!=m)if(x.key===m.key)h(c,x,m,f,p(a,q+1,e),g,n),g&&x.tag===m.tag&&r(c,v(x),e),null!=x.dom&&(e=x.dom),q--;else{if(!w){w=a;var x=q,D={},z;for(z=0;za.indexOf("?")?"?":"&";a+=f+d}return a}function v(a){try{return""!== +a?JSON.parse(a):null}catch(w){throw Error(a);}}function p(a){return a.responseText}function q(a,b){if("function"===typeof a)if(b instanceof Array)for(var d=0;dm.status||304===m.status)b(q(d.type,a));else{var f=Error(m.responseText),k;for(k in a)f[k]=a[k];h(f)}}catch(G){h(G)}}; +r&&null!=d.data?m.send(d.data):m.send()}))},jsonp:function(d){return b(new h(function(b,h){var m=d.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+n++,r=a.document.createElement("script");a[m]=function(f){r.parentNode.removeChild(r);b(q(d.type,f));delete a[m]};r.onerror=function(){r.parentNode.removeChild(r);h(Error("JSONP request failed"));delete a[m]};null==d.data&&(d.data={});d.url=f(d.url,d.data);d.data[d.callbackKey||"callback"]=m;r.src=k(d.url,d.data);a.document.documentElement.appendChild(r)}))}, +setCompletionCallback:function(a){m=a}}}(window,"undefined"!==typeof Promise?Promise:t),N=function(a){function h(g,c,a,b,d,f,h){for(;a=m&&u>=y;){var x=c[m],n=a[y];if(x!==n||e)if(null==x)m++;else if(null==n)y++;else if(x.key===n.key)m++,y++,k(g,x,n,d,p(c,m,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b);else if(x=c[z],x!==n||e)if(null==x)z--;else if(null==n)y++;else if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),(e||y= +m&&u>=y;){x=c[z];n=a[u];if(x!==n||e)if(null==x)z--;else{if(null!=n)if(x.key===n.key)k(g,x,n,d,p(c,z+1,b),e,f),e&&x.tag===n.tag&&q(g,v(x),b),null!=x.dom&&(b=x.dom),z--;else{if(!w){w=c;var x=z,D={},t;for(t=0;t