diff --git a/.eslintrc.js b/.eslintrc.js index 523eeb4c..25278201 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -182,7 +182,7 @@ module.exports = { "prefer-const": "error", "prefer-reflect": "off", "prefer-rest-params": "off", - "prefer-spread": "error", + "prefer-spread": "off", "prefer-template": "off", "quote-props": "off", "quotes": [ diff --git a/.travis.yml b/.travis.yml index 5cac2607..4d118c54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,12 @@ install: - npm install - npm install @alrra/travis-scripts@^3.0.1 +# Lint (but don't fail build) before running tests +before_script: npm run lint || true + +# Run more than just npm test +script: npm run build && npm test + # After a successful build create bundles & commit back to the repo after_success: - | @@ -29,8 +35,7 @@ after_success: --path-encrypted-key "./.deploy.enc" # Build & commit changes - $(npm bin)/commit-changes --commands "npm run build" \ - --commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \ + $(npm bin)/commit-changes --commit-message "Bundled output for commit $TRAVIS_COMMIT [skip ci]" \ --branch "$BRANCH" env: diff --git a/README.md b/README.md index e1be8c2b..8f547a14 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Mithril.js - A framework for building brilliant applications -[Installation](docs/installation.md) | [API](docs/api.md) | [Examples](docs/examples.md) | [Migration Guide](docs/v1.x-migration.md) +[Installation](docs/installation.md) | [API](docs/api.md) | [Examples](docs/examples.md) | [Changelog/Migration Guide](docs/change-log.md) -Note: This branch is the upcoming version 1.0. It's a rewrite from the ground up and it's not backwards compatible with [Mithril 0.2.x](http://mithril.js.org). You can find preliminary [documentation here](docs) and [migration guide here](docs/v1.x-migration.md) +Note: This branch is the upcoming version 1.0. It's a rewrite from the ground up and it's not backwards compatible with [Mithril 0.2.x](http://mithril.js.org). You can find preliminary [documentation here](docs) and [migration guide here](docs/change-log.md) This rewrite aims to fix longstanding API design issues, significantly improve performance, and clean up the codebase. @@ -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.43 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..0cebfcf6 100644 --- a/api/mount.js +++ b/api/mount.js @@ -1,23 +1,21 @@ "use strict" var Vnode = require("../render/vnode") -var autoredraw = require("../api/autoredraw") -module.exports = function(renderer, pubsub) { +module.exports = function(redrawService) { 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)) - }) - - run() + + var run = function() { + redrawService.render(root, Vnode(component)) + } + redrawService.subscribe(root, run) + redrawService.redraw() } } 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..3f98b061 --- /dev/null +++ b/api/redraw.js @@ -0,0 +1,47 @@ +"use strict" + +var coreRenderer = require("../render/render") + +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)) + } + } +} + +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, throttle(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..95ef6afd 100644 --- a/api/router.js +++ b/api/router.js @@ -3,55 +3,45 @@ 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 resolver, component, attrs, currentPath, resolve 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 update = function(routeResolver, comp, params, path) { + resolver = routeResolver, component = comp, attrs = params, currentPath = path, resolve = null + resolver.render = routeResolver.render || identity + render() + } + var render = function() { + if (resolver != null) redrawService.render(root, resolver.render(Vnode(component, attrs.key, attrs))) + } + routeService.defineRoutes(routes, function(payload, params, path) { + if (payload.view) update({}, payload, params, path) + else { + if (payload.onmatch) { + if (resolve != null) update(payload, component, params, path) + else { + resolve = function(resolved) { + update(payload, resolved, params, path) + } + payload.onmatch(function(resolved) { + if (resolve != null) resolve(resolved) + }, params, path) + } + } + else update(payload, "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, render) } - route.link = router.link - route.prefix = router.setPrefix - route.set = router.setPath + route.set = routeService.setPath route.get = function() {return currentPath} - + 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..210a0627 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) { @@ -161,7 +159,7 @@ o.spec("mount", function() { o("event handlers can skip redraw", function(done) { var onupdate = o.spy() - var oninit = o.spy() + var oninit = o.spy() var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -197,8 +195,8 @@ o.spec("mount", function() { mount(root, { view : function() { return m("div", { - oninit : oninit, - onupdate : onupdate + oninit: oninit, + onupdate: onupdate }) } }) @@ -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() { @@ -215,4 +213,26 @@ o.spec("mount", function() { done() }, FRAME_BUDGET) }) + + o("throttles", function(done, timeout) { + timeout(200) + + var i = 0 + mount(root, {view: function() {i++}}) + var before = i + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + + var after = i + + setTimeout(function(){ + o(before).equals(1) // mounts synchronously + o(after).equals(1) // throttles rest + o(i).equals(2) + done() + },40) + }) }) 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..f13c2d3f --- /dev/null +++ b/api/tests/test-redraw.js @@ -0,0 +1,97 @@ +"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(done) { + var spy = o.spy() + + redrawService.subscribe(root, spy) + + o(spy.callCount).equals(0) + + redrawService.redraw() + + 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() { + 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..09c46eee 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) @@ -167,8 +165,8 @@ o.spec("route", function() { o("event handlers can skip redraw", 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) @@ -240,25 +238,28 @@ o.spec("route", function() { } } + var resolver = { + onmatch: function(resolve, args, requestedPath) { + matchCount++ + + o(args.id).equals("abc") + o(requestedPath).equals("/abc") + o(this).equals(resolver) + resolve(Component) + }, + render: function(vnode) { + renderCount++ + + o(vnode.attrs.id).equals("abc") + o(this).equals(resolver) + + return vnode + }, + } + $window.location.href = prefix + "/abc" route(root, "/abc", { - "/:id" : { - onmatch: function(resolve, args, requestedPath) { - matchCount++ - - o(args.id).equals("abc") - o(requestedPath).equals("/abc") - - resolve(Component) - }, - render: function(vnode) { - renderCount++ - - o(vnode.attrs.id).equals("abc") - - return vnode - }, - }, + "/:id" : resolver }) o(matchCount).equals(1) @@ -403,7 +404,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 +506,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,22 +516,25 @@ 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()}) + var render = o.spy(function(){return m("div")}) $window.location.href = prefix + "/" route(root, '/', { - "/":{ + "/": { onmatch: onmatch, - render: function(){return m("div")} + render: render } }) o(onmatch.callCount).equals(1) + o(render.callCount).equals(1) route.set(route.get()) setTimeout(function() { o(onmatch.callCount).equals(2) + o(render.callCount).equals(2) done() }, FRAME_BUDGET) @@ -610,6 +614,53 @@ o.spec("route", function() { done() }, 30) }) + + o("route changes activate onbeforeremove", function(done, timeout) { + var spy = o.spy() + + $window.location.href = prefix + "/a" + route(root, "/a", { + "/a": { + onbeforeremove: spy, + view: function() {} + }, + "/b": { + view: function() {} + } + }) + + route.set("/b") + + setTimeout(function() { + o(spy.callCount).equals(1) + + done() + }, 30) + }) + + o("throttles", function(done, timeout) { + timeout(200) + + var i = 0 + $window.location.href = prefix + "/" + route(root, "/", { + "/": {view: function(v) {i++}} + }) + var before = i + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + var after = i + + setTimeout(function(){ + o(before).equals(1) // routes synchronously + o(after).equals(2) // redraws synchronously + o(i).equals(3) // throttles rest + done() + },40) + }) }) }) }) 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 84574a6d..34148951 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -15,6 +15,7 @@ function parse(file) { try {return JSON.parse(json)} catch (e) {throw new Error("invalid JSON: " + json)} } +var error function run(input, output) { try { var modules = {} @@ -31,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 }) @@ -56,8 +58,11 @@ function run(input, output) { var code = read(filepath) // if there's a syntax error, report w/ proper stack trace try {new Function(code)} catch (e) { - proc.exec("node " + filename, function(error) { - if (error !== null) console.log("\x1b[31m" + error.message) + proc.exec("node " + filepath, function(e) { + if (e !== null && e.message !== error) { + error = e.message + console.log("\x1b[31m" + e.message + "\x1b[0m") + } }) } @@ -111,7 +116,11 @@ function run(input, output) { code = "new function() {\n" + code + "\n}" - if (!isFile(output) || code !== read(output)) fs.writeFileSync(output, code, "utf8") + if (!isFile(output) || code !== read(output)) { + //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} + error = null + fs.writeFileSync(output, code, "utf8") + } } catch (e) { console.error(e.message) 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/docs/api.md b/docs/api.md index 9b301bdb..eb2a7b31 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,22 +1,174 @@ # API -- [m](hyperscript.md) -- [m.render](render.md) -- [m.mount](mount.md) -- [m.route](route.md) - - [m.route.set](route.md#routeset) - - [m.route.get](route.md#routeget) - - [m.route.prefix](route.md#routeprefix) - - [m.route.link](route.md#routelink) -- [m.request](request.md) -- [m.jsonp](jsonp.md) -- [m.parseQueryString](parseQueryString.md) -- [m.buildQueryString](buildQueryString.md) -- [m.withAttr](withAttr.md) -- [m.trust](trust.md) -- [m.fragment](fragment.md) -- [m.redraw](redraw.md) -- [m.version](version.md) +### Cheatsheet + +Here are examples for the most commonly used methods. If a method is not listed below, it's meant for advanced usage. + +#### m(selector, attrs, children) - [docs](hyperscript.md) + +```javascript +m("div.class#id", {title: "title"}, ["children"]) +``` + +--- + +#### m.mount(element, component) - [docs](mount.md) + +```javascript +var state = { + count: 0, + inc: function() {state.count++} +} + +var Counter = { + view: function() { + return m("div", {onclick: state.inc}, state.count) + } +} + +m.mount(document.body, Counter) +``` + +--- + +#### m.route(root, defaultRoute, routes) - [docs](route.md) + +```javascript +var Home = { + view: function() { + return "Welcome" + } +} + +m.route(document.body, "/home", { + "/home": Home, // defines `http://localhost/#!/home` +}) +``` + +#### m.route.set(path) - [docs](route.md#routeset) + +```javascript +m.route.set("/home") +``` + +#### m.route.get() - [docs](route.md#routeget) + +```javascript +var currentRoute = m.route.get() +``` + +#### m.route.prefix(prefix) - [docs](route.md#routeprefix) + +Call this before `m.route()` + +```javascript +m.route.prefix("#!") +``` + +#### m.route.link() - [docs](route.md#routelink) + +```javascript +m("a[href='/Home']", {oncreate: m.route.link}, "Go to home page") +``` + +--- + +#### m.request(options) - [docs](request.md) + +```javascript +m.request({ + method: "PUT", + url: "/api/v1/users/:id", + data: {id: 1, name: "test"} +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +#### m.jsonp(options) - [docs](jsonp.md) + +```javascript +m.jsonp({ + url: "/api/v1/users/:id", + data: {id: 1}, + callbackKey: "callback", +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +#### m.parseQueryString(querystring) - [docs](parseQueryString.md) + +```javascript +var object = m.parseQueryString("a=1&b=2") +// {a: "1", b: "2"} +``` + +--- + +#### m.buildQueryString(object) - [docs](buildQueryString.md) + +```javascript +var querystring = m.buildQueryString({a: "1", b: "2"}) +// "a=1&b=2" +``` + +--- + +#### m.withAttr(attrName, callback) - [docs](withAttr.md) + +```javascript +var state = { + value: "", + setValue: function(v) {value = v} +} + +var Component = { + view: function() { + return m("input", { + oninput: m.withAttr("value", state.setValue), + value: state.value, + }) + } +} + +m.mount(document.body, Component) +``` + +--- + +#### m.trust(htmlString) - [docs](trust.md) + +```javascript +m.render(document.body, m.trust("

Hello

")) +``` + +--- + +#### m.redraw() - [docs](redraw.md) + +```javascript +var count = 0 +function inc() { + setInterval(function() { + count++ + m.redraw() + }, 1000) +} + +var Counter = { + oninit: inc, + view: function() { + return m("div", count) + } +} + +m.mount(document.body, Counter) +``` -- [Promise](promise.md) -- [Stream](stream.md) \ No newline at end of file diff --git a/docs/buildQueryString.md b/docs/buildQueryString.md index 23d855a5..03f723fa 100644 --- a/docs/buildQueryString.md +++ b/docs/buildQueryString.md @@ -1,11 +1,23 @@ # buildQueryString(object) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Description + +Turns an object into a string of form `a=1&b=2` + +```javascript +var querystring = m.buildQueryString({a: "1", b: "2"}) +// "a=1&b=2" +``` + +--- + +### Signature `querystring = m.buildQueryString(object)` diff --git a/docs/v1.x-migration.md b/docs/change-log.md similarity index 77% rename from docs/v1.x-migration.md rename to docs/change-log.md index f23111b1..2698ad9d 100644 --- a/docs/v1.x-migration.md +++ b/docs/change-log.md @@ -1,7 +1,15 @@ -# Migrating from `v0.2.x` to `v1.x` +# Change log + +- [Migrating from v0.2.x](#migrating-from-v02x) + +--- + +### Migrating from `v0.2.x` `v1.x` is largely API-compatible with `v0.2.x`, but there are some breaking changes. +If you are migrating, consider using the [mithril-codemods](https://www.npmjs.com/package/mithril-codemods) tool to help automate the most straightforward migrations. + - [`m.prop` removed](#mprop-removed) - [`m.component` removed](#mcomponent-removed) - [`config` function](#config-function) @@ -15,14 +23,20 @@ - [`m.route` and anchor tags](#mroute-and-anchor-tags) - [Reading/writing the current route](#readingwriting-the-current-route) - [Accessing route params](#accessing-route-params) +- [Preventing unmounting](#preventing-unmounting) - [`m.request`](#mrequest) +- [`m.sync` removed](#msync-removed) - [`xlink` namespace required](#xlink-namespace-required) +- [Nested arrays in views](#nested-arrays-in-views) +- [`vnode` equality checks](#vnode-equality-checks) +- [`m.startComputation`/`m.endComputation` removed](#mstartcomputationmendcomputation-removed) +- [Synchronous redraw removed](#synchronous-redraw-removed) --- ## `m.prop` removed -In `v1.x`, `m.prop` is now a more powerful stream micro-library, but it's no longer part of core. +In `v1.x`, `m.prop()` is now a more powerful stream micro-library, but it's no longer part of core. ### `v0.2.x` @@ -243,7 +257,7 @@ m.mount(document.body, { oninit : function(vnode) { // ... }, - + view : function(vnode) { // Use vnode.state instead of ctrl // Use vnode.attrs instead of options @@ -406,9 +420,42 @@ m.route(document.body, "/booga", { --- +## Preventing unmounting + +It is no longer possible to prevent unmounting via `onunload`'s `e.preventDefault()`. Instead you should explicitly call `m.route.set` when the expected conditions are met. + +### `v0.2.x` + +```javascript +var Component = { + controller: function() { + this.onunload = function(e) { + if (condition) e.preventDefault() + } + }, + view: function() { + return m("a[href=/]", {config: m.route}) + } +} +``` + +### `v1.x` + +```javascript +var Component = { + view: function() { + return m("a", {onclick: function() {if (!condition) m.route.set("/")}}) + } +} +``` + +--- + ## m.request -Promises returned by [m.request](request.md) are no longer `m.prop` getter-setters. In addition, `initialValue` is no longer a supported option. +Promises returned by [m.request](request.md) are no longer `m.prop` getter-setters. In addition, `initialValue`, `unwrapSuccess` and `unwrapError` are no longer supported options. + +In addition, requests no longer have `m.startComputation`/`m.endComputation` semantics. Instead, redraws are always triggered when a request promise chain completes (unless `background:true` is set). ### `v0.2.x` @@ -441,7 +488,13 @@ setTimeout(function() { }, 1000) ``` -The equivalent of `m.sync` is now `Promise.all` +Additionally, if the `extract` option is passed to `m.request` the return value of the provided function will be used directly to resolve its promise, and the `deserialize` callback is ignored. + +--- + +## `m.sync` removed + +`m.sync` has been removed in favor of `Promise.all` ### `v0.2.x` @@ -467,8 +520,6 @@ Promise.all([ }) ``` -Additionally, if the `extract` option is passed to `m.request` the return value of the provided function will be used directly to resolve its promise, and the `deserialize` callback is ignored. - --- ## `xlink` namespace required @@ -492,3 +543,39 @@ m("svg", m("image[xlink:href='image.gif']") ) ``` + +--- + +## Nested arrays in views + +Arrays now represent [fragments](fragment.md), which are structurally significant in v1.x virtual DOM. Whereas nested arrays in v0.2.x would be flattened into one continuous list of virtual nodes for the purposes of diffing, v1.x preserves the array structure - the children of any given array are not considered siblings of those of adjacent arrays. + +--- + +## `vnode` equality checks + +If a vnode is strictly equal to the vnode occupying its place in the last draw, v1.x will skip that part of the tree without checking for mutations or triggering any lifecycle methods in the subtree. The component documentation contains [more detail on this issue](components.md#avoid-creating-component-instances-outside-views). + +--- + +## `m.startComputation`/`m.endComputation` removed + +They are considered anti-patterns and have a number of problematic edge cases, so they no longer exist in v1.x + +--- + +## Synchronous redraw removed + +In v0.2.x it was possible to force mithril to redraw immediately by passing a truthy value to `m.redraw()`. This behavior complicated usage of `m.redraw()` and caused some hard-to-reason about issues and has been removed. + +### `v0.2.x` + +```javascript +m.redraw(true); // redraws immediately & synchronously +``` + +### `v1.x` + +```javascript +m.redraw(); // schedules a redraw on the next requestAnimationFrame tick +``` diff --git a/docs/components.md b/docs/components.md index bf61a6ee..ee74bd5c 100644 --- a/docs/components.md +++ b/docs/components.md @@ -173,7 +173,7 @@ Although Mithril is flexible, some code patterns are discouraged: #### Avoid restrictive interfaces -A component has a restrictive interface when it exposes only specific properties, under the assumption that other properties will not be needed, or that they can be added at a later time. +Try to keep component interfaces generic - using `attrs` and `children` directly - unless the component requires special logic to operate on input. In the example below, the `button` configuration is severely limited: it does not support any events other than `onclick`, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML). @@ -188,7 +188,7 @@ var RestrictiveComponent = { } ``` -It's preferable to allow passing through parameters to a component's root node, if it makes sense to do so: +If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node. ```javascript // PREFER @@ -201,7 +201,9 @@ var FlexibleComponent = { } ``` -#### Avoid magic indexes +#### Don't manipulate `children` + +However, if a component is opinionated in how it applies attributes or children, you should switch to using custom attributes. Often it's desirable to define multiple sets of children, for example, if a component has a configurable title and body. @@ -233,7 +235,7 @@ m(Header, [ ]) ``` -The component above makes different children look different based on where they appear in the array. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content: +The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content: ```javascript // PREFER @@ -261,7 +263,9 @@ m(BetterHeader, { }) ``` -#### Avoid component factories +#### Define components statically, call them dynamically + +##### Avoid creating component definitions inside views If you create a component from within a `view` method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch. @@ -291,3 +295,65 @@ m.render(document.body, m(Component, {greeting: "hello"})) // calling a second time does not modify DOM m.render(document.body, m(Component, {greeting: "hello"})) ``` + +##### Avoid creating component instances outside views + +Conversely, for similar reasons, if a component instance is created outside of a view, future redraws will perform an equality check on the node and skip it. Therefore component instances should always be created inside views: + +```javascript +// AVOID +var Counter = { + count: 0, + view: function(vnode) { + return m("div", + m("p", "Count: " + vnode.state.count ), + + m("button", { + onclick: function() { + vnode.state.count++ + } + }, "Increase count") + ) + } +} + +var counter = m(Counter) + +m.mount(document.body, { + view: function(vnode) { + return [ + m("h1", "My app"), + counter + ] + } +}) +``` + +In the example above, clicking the counter component button will increase its state count, but its view will not be triggered because the vnode representing the component shares the same reference, and therefore the render process doesn't diff them. You should always call components in the view to ensure a new vnode is created: + +```javascript +// PREFER +var Counter = { + count: 0, + view: function(vnode) { + return m("div", + m("p", "Count: " + vnode.state.count ), + + m("button", { + onclick: function() { + vnode.state.count++ + } + }, "Increase count") + ) + } +} + +m.mount(document.body, { + view: function(vnode) { + return [ + m("h1", "My app"), + m(Counter) + ] + } +}) +``` diff --git a/CONTRIBUTING.md b/docs/contributing.md similarity index 58% rename from CONTRIBUTING.md rename to docs/contributing.md index 044e4ffa..e2e65fd6 100644 --- a/CONTRIBUTING.md +++ b/docs/contributing.md @@ -4,21 +4,38 @@ ## How do I go about contributing ideas or new features? -Create an issue to suggest it and discuss first. Avoid submitting large changes. +Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. And don't worry, we're nice :) + +If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities. ## How should I report bugs? -Ideally, provide code to reproduce the issue (via jsfiddle, a gist, etc). Even better, submit a pull request with a fix and tests. If you don't know how to test your fix, or lint or whatever, submit anyways, and we can help you. +Ideally, the best way to report bugs is to provide a small snippet of code where the issue can be reproduced (via jsfiddle, jsbin, a gist, etc). Even better would be to submit a pull request with a fix and tests. If you don't know how to test your fix, or lint or whatever, submit anyways, and we can help you. -## How do I run tests? +## How do I send a pull request? -Assuming you have forked this repo, you can open the `index.html` file in a module's `tests` folder and look at console output to see only tests for that module, or you can run `ospec/bin/ospec` from the command line to run all tests under a Node.js environment. Additionally, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test. +To send a pull request: -There is no need to `npm install` anything in order to run the test suite, however NodeJS is required to run the test suite from the command line. +- fork the repo (button at the top right in Github) +- clone the forked repo to your computer (green button in Github) +- create a feature branch (run `git checkout -b the-feature-branch-name`) +- make your changes +- run the tests (run `npm t`) +- submit a pull request (go to the pull requests tab in Github, click the green button and select your feature branch) + + + +## I'm submitting a PR. How do I run tests? + +Assuming you have forked this repo, you can open the `index.html` file in a module's `tests` folder and look at console output to see only tests for that module, or you can run `ospec/bin/ospec` from the command line to run all tests. + +While testing, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test to speed up your debugging experience. Don't forget to remove the `.only` after you're done! + +There is no need to `npm install` anything in order to run the test suite, however NodeJS is required to run the test suite from the command line. You do need to `npm install` if you want to lint or get a code coverage report though. @@ -26,7 +43,15 @@ There is no need to `npm install` anything in order to run the test suite, howev If all you're trying to do is run examples in the codebase, you don't need to build Mithril, you can just open the various html files and things should just work. -To generate the bundled file, run `node bundler/bundler.js` from the command line. There is no need to `npm install` anything, but NodeJS is required to run the build script. +To generate the bundled file for testing, run `npm run dev` from the command line. To generate the minified file, run `npm run build`. There is no need to `npm install` anything, but NodeJS is required to run the build scripts. + + + +## Is there a style guide? + +Yes, there's an `eslint` configuration, but it's not strict about formatting at all. If your contribution passes `npm run lint`, it's good enough for a PR (and it can still be accepted even if it doesn't pass). + +Spacing and formatting inconsistencies may be fixed after the fact, and we don't want that kind of stuff getting in the way of contributing. @@ -40,11 +65,11 @@ Another important reason is that it allows us to document browser API quirks via ## Why does Mithril use its own testing framework and not Mocha/Jasmine/Tape? -Mainly to avoid requiring dependencies. ospec is customized to provide only essential information for common testing workflows (namely, no spamming ok's on pass, and accurate noiseless errors on failure) +Mainly to avoid requiring dependencies. `ospec` is customized to provide only essential information for common testing workflows (namely, no spamming ok's on pass, and accurate noiseless errors on failure) -## Why do tests and examples use `module/module.js`? Why not use Browserify, Webpack or Rollup? +## Why do tests use `module/module.js`? Why not use Browserify, Webpack or Rollup? Again, to avoid requiring dependencies. The Mithril codebase is written using a statically analyzable subset of CommonJS module definitions (as opposed to ES6 modules) because its syntax is backwards compatible with ES5, therefore making it possible to run source code unmodified in browsers without the need for a build tool or a file watcher. diff --git a/docs/examples.md b/docs/examples.md index d52df077..140ad3f6 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,8 +1,11 @@ # Examples +Here are some examples of Mithril in action + - [Animation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/animation/mosaic.html) - [DBMonster](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) - [Markdown Editor](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/editor/index.html) - SVG: [Clock](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/tiger.html) - [ThreadItJS](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/threaditjs/index.html) -- [TodoMVC](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/todomvc/index.html) \ No newline at end of file +- [TodoMVC](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/todomvc/index.html) + diff --git a/docs/fragment.md b/docs/fragment.md index fa04799b..cd1ff53a 100644 --- a/docs/fragment.md +++ b/docs/fragment.md @@ -1,13 +1,20 @@ -# fragment(html) +# fragment(attrs, children) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Description -Generates a trusted HTML [vnode](vnodes.md) +Allows attaching lifecycle methods to a fragment [vnode](vnodes.md) + +--- + +### Signature + +Generates a fragment [vnode](vnodes.md) `vnode = m.fragment(attrs, children)` diff --git a/docs/generate.js b/docs/generate.js new file mode 100644 index 00000000..f8763522 --- /dev/null +++ b/docs/generate.js @@ -0,0 +1,53 @@ +var fs = require("fs") +var path = require("path") +var marked = require("marked") +var layout = fs.readFileSync("./docs/layout.html", "utf-8") +var version = JSON.parse(fs.readFileSync("./package.json", "utf-8")).version +try {fs.mkdirSync("docs/archive/")} catch (e) {} +try {fs.mkdirSync("docs/archive/" + version)} catch (e) {} +try {fs.mkdirSync("docs/archive/" + version + "/lib")} catch (e) {} +try {fs.mkdirSync("docs/archive/" + version + "/lib/prism")} catch (e) {} + +var guides = fs.readFileSync("docs/guides.md", "utf-8") +var methods = fs.readFileSync("docs/methods.md", "utf-8") + +generate("docs") + +function generate(pathname) { + if (fs.lstatSync(pathname).isDirectory()) { + fs.readdirSync(pathname).forEach(function(filename) { + generate(pathname + "/" + filename) + }) + } + else if (!pathname.match(/tutorials|archive/)) { + if (pathname.match(/\.md$/)) { + var outputFilename = pathname.replace(/\.md$/, ".html") + var markdown = fs.readFileSync(pathname, "utf-8") + var fixed = markdown + .replace(/(`[^`]+?)<(.*`)/gim, "$1<$2") // fix generic syntax + .replace(/`((?:\S| -> |, )+)(\|)(\S+)`/gim, function(match, a, b, c) { // fix pipes in code tags + return "" + (a + b + c).replace(/\|/g, "|") + "" + }) + .replace(/(^# .+?(?:\r?\n){2,}?)(?:(-(?:.|\r|\n)+?)((?:\r?\n){2,})|)/m, function(match, title, nav, space) { // inject menu + var file = path.basename(pathname) + var link = new RegExp("([ \t]*)(- )(\\[.+?\\]\\(" + file + "\\))") + var replace = function(match, space, li, link) { + return space + li + "**" + link + "**" + (nav ? "\n" + nav.replace(/(^|\n)/g, "$1\t" + space) : "") + } + var modified = guides.match(link) ? guides.replace(link, replace) : methods.replace(link, replace) + return title + modified + "\n\n" + }) + .replace(/\.md/gim, ".html") // fix links + var html = layout + .replace(/\[body\]/, marked(fixed)) + .replace(/
([^<]+?)<\/h5>/gim, function(match, id, text) { // fix anchors + return "
" + text + "
" + }) + fs.writeFileSync("docs/archive/" + version + "/" + outputFilename.replace(/^docs\//, ""), html, "utf-8") + } + else { + fs.writeFileSync("docs/archive/" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") + } + } +} + diff --git a/docs/guides.md b/docs/guides.md new file mode 100644 index 00000000..4abbf045 --- /dev/null +++ b/docs/guides.md @@ -0,0 +1,17 @@ +- Tutorials + - [Installation](installation.md) + - [Introduction](introduction.md) + - [Tutorial](tutorial.md) + - [Testing](testing.md) + - [Examples](examples.md) +- Key concepts + - [Vnodes](vnodes.md) + - [Components](components.md) + - [Lifecycle methods](lifecycle-methods.md) + - [Keys](keys.md) +- Social + - [Community chat](https://gitter.im/lhorie/mithril.js) + - [Contributing](contributing.md) + - [Credits](credits.md) +- Misc + - [Change log/Migration](change-log.md) diff --git a/docs/hyperscript.md b/docs/hyperscript.md index af1067e9..9d608dcd 100644 --- a/docs/hyperscript.md +++ b/docs/hyperscript.md @@ -1,6 +1,7 @@ # m(selector, attributes, children) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Flexibility](#flexibility) - [CSS selectors](#css-selectors) @@ -17,13 +18,31 @@ --- -### API +### Description + +Represents an HTML element in a Mithril view + +```javascript +m("div", {class: "foo"}, "hello") +// represents
hello
+``` + +You can also [use HTML syntax](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E) via a Babel plugin. + +```markup +/** jsx m */ +
hello
+``` + +--- + +### Signature `vnode = m(selector, attributes, children)` Argument | Type | Required | Description ------------ | ------------------------------------------ | -------- | --- -`selector` | `String|Object` | Yes | A CSS selector or a [component](https://github.com/lhorie/mithril.js/blob/rewrite/docs/components.md) +`selector` | `String|Object` | Yes | A CSS selector or a [component](components.md) `attributes` | `Object` | No | HTML attributes or element properties `children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats) **returns** | `Vnode` | | A [vnode](vnodes.md#structure) @@ -408,3 +427,9 @@ var BetterListComponent = { } } ``` + +#### Avoid creating vnodes outside views + +When a redraw encounters a vnode which is strictly equal to the one in the previous render, it will be skipped and its contents will not be updated. While this may seem like an opportunity for performance optimisation, it should be avoided because it prevents dynamic changes in that node's tree - this leads to side-effects such as downstream lifecycle methods failing to trigger on redraw. In this sense, Mithril vnodes are immutable: new vnodes are compared to old ones; mutations to vnodes are not persisted. + +The component documentation contains [more detail and an example of this anti-pattern](components.md#avoid-creating-component-instances-outside-views). diff --git a/docs/installation.md b/docs/installation.md index 51c0848e..4009bd28 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,32 +1,52 @@ # Installation +### CDN + +If you're new to Javascript or just want a very simple setup to get your feet wet, you can get Mithril from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network): + +```markup + +``` + +--- + ### NPM #### Quick start -``` -#install +```bash +# 1) install npm install mithril@rewrite --save -# add this line into the scripts section in package.json +# 2) add this line into the scripts section in package.json # "scripts": { # "build": "bundle index.js --output app.js --watch" # } -# create an `index.js` file +# 3) create an `index.js` file -# run bundler +# 4) run bundler npm run build ``` #### Step by step +For production-level projects, the recommended way of installing Mithril is to use NPM. + NPM (Node package manager) is the default package manager that is bundled w/ Node.js. It is widely used as the package manager for both client-side and server-side libraries in the Javascript ecosystem. Download and install [Node.js](https://nodejs.org); NPM will be automatically installed as well. -To use Mithril via NPM: +To use Mithril via NPM, go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`. -- go to your project folder, and run `npm init --yes` from the command line. This will create a file called `package.json`. -- run `npm install mithril@rewrite --save`. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file +```bash +npm init --yes +# creates a file called package.json +``` + +Then, run `npm install mithril@rewrite --save` to install Mithril. This will create a folder called `node_modules`, and a `mithril` folder inside of it. It will also add an entry under `dependencies` in the `package.json` file + +```bash +npm install mithril@rewrite --save +``` You are now ready to start using Mithril. The recommended way to structure code is to modularize it via CommonJS modules: @@ -129,13 +149,6 @@ webpack --watch If you don't have the ability to run a bundler script due to company security policies, there's an options to not use a module system at all: -```javascript -// index.js - -// if a CommonJS environment is not detected, Mithril will be created in the global scope -m.render(document.body, "hello world") -``` - ```markup @@ -147,3 +160,10 @@ m.render(document.body, "hello world") ``` + +```javascript +// index.js + +// if a CommonJS environment is not detected, Mithril will be created in the global scope +m.render(document.body, "hello world") +``` diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 00000000..86f5d329 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,238 @@ +# Introduction + +- [What is Mithril?](#what-is-mithril) +- [Getting started](#getting-started) +- [Hello world](#hello-world) +- [DOM elements](#dom-elements) +- [Components](#components) +- [Routing](#routing) +- [XHR](#xhr) + +--- + +### What is Mithril? + +Mithril is a framework for building Single Page Applications. It's small but batteries-included. + +--- + +### Getting started + +The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface but it'll only take 10 minutes. + +Let's create an HTML file to follow along: + +```markup + + + +``` + +--- + +### Hello world + +Let's start as small as well can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better) + +```javascript +var root = document.body + +m.render(root, "Hello world") +``` + +Now, let's change the text to something else. Add this line of code under the previous one: + +```javascript +m.render(root, "My first app") +``` + +As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch. + +--- + +### DOM elements + +Let's wrap our text in an `

` tag. + +```javascript +m.render(root, m("h1", "My first app")) +``` + +The `m()` function can be used to describe any HTML structure you want. So if you to add a class to the `

`: + +```javascript +m("h1", {class: "title"}, "My first app") +``` + +If you want to have multiple elements: + +```javascript +[ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), +] +``` + +And so on: + +```javascript +m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), +]) +``` + +Note: If you prefer `` syntax, [it's possible via Babel](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m%20*%2F%0A%3Ch1%3EMy%20first%20app%3C%2Fh1%3E). + +```markup +// HTML syntax via Babel's JSX plugin +
+

My first app

+ +
+``` + +--- + +### Components + +A Mithril component is just an object with a `view` function. Here's the code above as a component: + +```javascript +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", "A button"), + ]) + } +} +``` + +To activate the component, we use `m.mount`. + +```javascript +m.mount(root, Hello) +``` + +As you would expect, doing so creates this markup: + +```markup +
+

My first app

+ +
+``` + +The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events: + +```javascript +var count = 0 // added a variable + +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", {onclick: function() {count++}}, count + " clicks"), // changed this line + ]) + } +} + +m.mount(root, Hello) +``` + +We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label. + +You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril does it for you. + +If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates. + +--- + +### Routing + +Routing just means going from one screen to another in an application with several screens. + +Let's add a splash page that appears before our click counter. First we create a component for it: + +```javascript +var Splash = { + view: function() { + return m("a", {href: "#!/hello"}, "Enter!") + } +} +``` + +As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path. + +Now that we going to have more than one screen, we use `m.route` instead of `m.mount`. + +```javascript +m.route(root, "/splash", { + "/splash": Splash, + "/hello": Hello, +}) +``` + +The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a `#!` in the URL. + +The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is `http://localhost`, then you get redirected to `http://localhost/#!/splash`. + +Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `http://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button. + +--- + +### XHR + +Basically, XHR is just a way to talk to a server. + +Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial. + +First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `data` is the payload that we're sending to the endpoint and `useCredentials` means to enable cookies (a requirement for the REM API to work) + +```javascript +var count = 0 +var increment = function() { + m.request({ + method: "PUT", + url: "http://rem-rest-api.herokuapp.com/api/tutorial/1", + data: {count: count + 1}, + useCredentials: true, + }) + .then(function(data) { + count = parseInt(data.count) + }) +} +``` + +Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now. + +Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly: + +```javascript +var Hello = { + view: function() { + return m("main", [ + m("h1", {class: "title"}, "My first app"), + m("button", {onclick: increment}, count + " clicks"), + ]) + } +} +``` + +Clicking the button should now update the count. + +--- + +We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR. + +This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](simple-application.md), which walks you through building a realistic application. + + + + + diff --git a/docs/jsonp.md b/docs/jsonp.md index 185212a0..1e2300f7 100644 --- a/docs/jsonp.md +++ b/docs/jsonp.md @@ -1,19 +1,38 @@ # jsonp(options) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) --- -### API +### Description -`promise = m.jsonp(options)` +Makes JSON-P requests. Typically, it's useful to interact with servers that allow JSON-P but that don't have CORS enabled. + +```javascript +m.jsonp({ + url: "/api/v1/users/:id", + data: {id: 1}, + callbackKey: "callback", +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +### Signature + +`promise = m.jsonp([url,] options)` Argument | Type | Required | Description ---------------------- | --------------------------------- | -------- | --- +`url` | `String` | No | If present, it's equivalent to having the option `{url: url}`. Values passed to the `options` argument override options set via this shorthand. `options.url` | `String` | Yes | The URL to send the request to. The URL may be either absolute or relative, and it may contain [interpolations](#dynamic-urls). -`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring (for GET requests) or body (for other types of requests). +`options.data` | `any` | No | The data to be interpolated into the URL and serialized into the querystring. `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.callbackName` | `String` | No | The name of the function that will be called as the callback. Defaults to a randomized string (e.g. `_mithril_6888197422121285_0({a: 1})` `options.callbackKey` | `String` | No | The name of the querystring parameter name that specifies the callback name. Defaults to `callback` (e.g. `/someapi?callback=_mithril_6888197422121285_0`) diff --git a/docs/layout.html b/docs/layout.html new file mode 100644 index 00000000..6e96bb1b --- /dev/null +++ b/docs/layout.html @@ -0,0 +1,29 @@ + + + + Mithril.js + + + + + +
+
+

Mithril

+ +
+
+
+
+ [body] +
+ License: MIT. © Leo Horie. +
+
+ + +e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; +Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; +Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; +; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; diff --git a/docs/lint.js b/docs/lint.js index 240eb368..576dd181 100644 --- a/docs/lint.js +++ b/docs/lint.js @@ -86,6 +86,9 @@ function initMocks() { "GET /api/v1/todos": function(request) { return {status: 200, responseText: JSON.stringify([])} }, + "PUT /api/v1/users/1": function(request) { + return {status: 200, responseText: request.query.callback ? request.query.callback + "([])" : "[]"} + }, "POST /api/v1/upload": function(request) { return {status: 200, responseText: JSON.stringify([])} }, @@ -132,11 +135,11 @@ function traverseDirectory(pathname, callback) { //run traverseDirectory("./docs", function(pathname) { - if (pathname.indexOf(".md") > -1 && !pathname.match(/migration|zero|simple|node_modules/)) { + if (pathname.indexOf(".md") > -1 && !pathname.match(/change-log|node_modules/)) { fs.readFile(pathname, "utf8", function(err, data) { if (err) console.log(err) else lint(pathname, data) }) } }) - +.then(process.exit) diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 00000000..9b19b4cc --- /dev/null +++ b/docs/methods.md @@ -0,0 +1,20 @@ +- Core + - [m](hyperscript.md) + - [m.render](render.md) + - [m.mount](mount.md) + - [m.route](route.md) + - [m.request](request.md) + - [m.jsonp](jsonp.md) + - [m.parseQueryString](parseQueryString.md) + - [m.buildQueryString](buildQueryString.md) + - [m.withAttr](withAttr.md) + - [m.trust](trust.md) + - [m.fragment](fragment.md) + - [m.redraw](redraw.md) + - [m.version](version.md) + - [Promise](promise.md) +- Optional + - [Stream](stream.md) +- Tooling + - [Bundler](bundler.md) + - [Ospec](ospec.md) diff --git a/docs/mount.md b/docs/mount.md index a402e1e6..a188668b 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -1,13 +1,35 @@ # mount(root, component) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Performance considerations](#performance-considerations) - [Differences from m.render](#differences-from-m-render) --- -### API +### Description + +Activates a component, enabling it to autoredraw on user events + +```javascript +var state = { + count: 0, + inc: function() {state.count++} +} + +var Counter = { + view: function() { + return m("div", {onclick: state.inc}, state.count) + } +} + +m.mount(document.body, Counter) +``` + +--- + +### Signature `m.mount(element, component)` diff --git a/docs/parseQueryString.md b/docs/parseQueryString.md index 9d159c35..e3d23dcb 100644 --- a/docs/parseQueryString.md +++ b/docs/parseQueryString.md @@ -1,11 +1,23 @@ # parseQueryString(string) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Description + +Turns a string of the form `?a=1&b=2` to an object + +```javascript +var object = m.parseQueryString("a=1&b=2") +// {a: "1", b: "2"} +``` + +--- + +### Signature `object = m.parseQueryString(string)` diff --git a/docs/promise.md b/docs/promise.md index 4cca9b48..119d3a51 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -1,24 +1,34 @@ # Promise(executor) -- [API](#api) -- [Static members](#static-members) - - [Promise.resolve](#promiseresolve) - - [Promise.reject](#promisereject) - - [Promise.all](#promiseall) - - [Promise.race](#promiserace) -- [Instance members](#static-members) - - [promise.then](#promisethen) - - [promise.catch](#promisecatch) +- [Description](#description) +- [Signature](#signature) + - [Static members](#static-members) + - [Promise.resolve](#promiseresolve) + - [Promise.reject](#promisereject) + - [Promise.all](#promiseall) + - [Promise.race](#promiserace) + - [Instance members](#instance-members) + - [promise.then](#promisethen) + - [promise.catch](#promisecatch) - [How it works](#how-it-works) - [Promise chaining](#promise-chaining) - [Promise absorption](#promise-absorption) - [Error handling](#error-handling) - [Shorthands](#shorthands) -- [Waiting for multiple promises](#waiting-for-multiple-promises) +- [Multiple promises](#multiple-promises) +- [Why not callbacks](#why-not-callbacks) --- -### API +### Description + +A [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) polyfill. + +A Promise is a mechanism for working with asynchronous computations. + +--- + +### Signature `promise = new Promise(executor)` @@ -146,7 +156,7 @@ promise.then(function(value) { }) ``` -Promises are useful for working with [asynchronous](https://en.wikipedia.org/wiki/Asynchrony_(computer_programming)) APIs, such as [`m.request`](request.md) +Promises are useful for working with asynchronous APIs, such as [`m.request`](request.md) Asynchronous APIs are those which typically take a long time to run, and therefore would take too long to return a value using the `return` statement of a function. Instead, they do their work in the background, allowing other Javascript code to run in the meantime. When they are done, they call a function with their results. @@ -262,7 +272,7 @@ promise --- -### Waiting for multiple promises +### Multiple promises In some occasions, you may need to make HTTP requests in parallel, and run code after all requests complete. This can be accomplished by `Promise.all` @@ -287,3 +297,13 @@ Promise.all([ In the example above, there are two user searches happening in parallel. Once they both complete, we take the names of all the users and alert them. This example also illustrates another benefit of smaller functions: we reused the `getUserNames` function we had created above. + +--- + +### Why not callbacks + +Callbacks are another mechanism for working with asynchrounous computations, and are indeed more adequate to use if an asynchronous computation may occur more than one time (for example, an `onscroll` event handler). + +However, for asynchronous computations that only occur once in response to an action, promises can be refactored more effectively, reducing code smells known as pyramids of doom (deeply nested series of callbacks with unmanaged state being used across several closure levels). + +In addition, promises can considerably reduce boilerplate related to error handling. \ No newline at end of file diff --git a/docs/redraw.md b/docs/redraw.md index fa80a5a4..ebc17283 100644 --- a/docs/redraw.md +++ b/docs/redraw.md @@ -1,11 +1,22 @@ # redraw() -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Description + +Updates the DOM after a change in the application data layer. + +You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril view, or after request completion when using `m.request`/`m.jsonp`. + +You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries. + +--- + +### Signature `m.redraw()` @@ -19,6 +30,6 @@ Argument | Type | Required | Description When callbacks outside of Mithril run, you need to notify Mithril's rendering engine that a redraw is needed. External callbacks could be `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, web socket library callbacks, event handlers in jQuery plugins, third party XHR request callbacks, etc. -To trigger a redraw, call `m.redraw()` +To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw. You should not call m.redraw from a [lifecycle method](lifecycle-methods.md). Doing so will result in undefined behavior. \ No newline at end of file diff --git a/docs/render.md b/docs/render.md index 60eaf784..3218c761 100644 --- a/docs/render.md +++ b/docs/render.md @@ -1,6 +1,7 @@ # render(element, vnodes) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Why Virtual DOM](#why-virtual-dom) - [Differences from other API methods](#differences-from-other-api-methods) @@ -8,7 +9,18 @@ --- -### API +### Description + +Renders a template to the DOM + +```javascript +m.render(document.body, "hello") +// hello +``` + +--- + +### Signature `m.render(element, vnodes)` @@ -52,6 +64,8 @@ Another difference is that `m.render` method expects a [vnode](vnodes.md) (or a ### Standalone usage +`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. Despite being incredibly small, the render module is fully functional and self-suficient. 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/request.md b/docs/request.md index 173233c1..cd204844 100644 --- a/docs/request.md +++ b/docs/request.md @@ -1,6 +1,7 @@ # request(options) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Typical usage](#typical-usage) - [Loading icons and error messages](#loading-icons-and-error-messages) @@ -10,12 +11,31 @@ - [Monitoring progress](#monitoring-progress) - [Casting response to a type](#casting-response-to-a-type) - [Non-JSON responses](#non-json-responses) +- [Retrieving response details](#retrieving-response-details) - [Why JSON instead of HTML](#why-json-instead-of-html) -- [Why XMLHttpRequest instead of fetch](#why-xmlhttprequest-instead-of-fetch) +- [Why XHR instead of fetch](#why-xhr-instead-of-fetch) +- [Avoid anti-patterns](#avoid-anti-patterns) --- -### API +### Description + +Makes XHR (aka AJAX) requests, and returns a [promise](promise.md) + +```javascript +m.request({ + method: "PUT", + url: "/api/v1/users/:id", + data: {id: 1, name: "test"} +}) +.then(function(result) { + console.log(result) +}) +``` + +--- + +### Signature `promise = m.request([url,] options)` @@ -35,6 +55,7 @@ Argument | Type | Required | Descr `options.deserialize` | `any = Function(string)` | No | A deserialization method to be applied to the response. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses. `options.extract` | `string = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for reading response headers and cookies. Defaults to a function that returns `xhr.responseText`. If defined, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. If a custom `extract` callback is set, `options.deserialize` is ignored and the string returned from the extract callback will not be parsed as JSON. `options.useBody` | `Boolean` | No | Force the use of the HTTP body section for `data` in `GET` requests when set to `true`, or the use of querystring for other HTTP methods when set to `false`. Defaults to `false` for `GET` requests and `true` for other methods. +`options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`. **returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods [How to read signatures](signatures.md) @@ -421,7 +442,7 @@ Data services may be organized in many different ways depending on the nature of --- -### Why XMLHttpRequest instead of fetch +### Why XHR instead of fetch [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is a newer Web API for fetching resources from servers, similar to `XMLHttpRequest`. @@ -430,11 +451,13 @@ Mithril's `m.request` uses `XMLHttpRequest` instead of `fetch()` for a number of - `fetch` is not fully standardized yet, and may be subject to specification changes. - `XMLHttpRequest` calls can be aborted before they resolve (e.g. to avoid race conditions in for instant search UIs). - `XMLHttpRequest` provides hooks for progress listeners for long running requests (e.g. file uploads). -- `XMLHttpRequest` is supported by all browsers, whereas `fetch()` is not supported by Internet Explorer and Safari. +- `XMLHttpRequest` is supported by all browsers, whereas `fetch()` is not supported by Internet Explorer, Safari and Android (non-Chromium). -Currently, due to lack of browser support, `fetch()` typically requires a [polyfill](https://github.com/github/fetch), which is over 11kb uncompressed - nearly three times larger than Mithril's `m.request`. +Currently, due to lack of browser support, `fetch()` typically requires a [polyfill](https://github.com/github/fetch), which is over 11kb uncompressed - nearly three times larger than Mithril's XHR module. -Despite being much smaller, `m.request` supports many important and not-so-trivial-to-implement features like [URL interpolation](#dynamic-urls), querystring serialization and [JSON-P requests](jsonp.md). The `fetch` polyfill does not support any of those. +Despite being much smaller, Mithril's XHR module supports many important and not-so-trivial-to-implement features like [URL interpolation](#dynamic-urls), querystring serialization and [JSON-P requests](jsonp.md), in addition to its ability to integrate seamlessly to Mithril's autoredrawing subsystem. The `fetch` polyfill does not support any of those, and requires extra libraries and boilerplates to achieve the same level of functionality. + +In addition, Mithril's XHR module is optimized for JSON-based endpoints and makes that most common case appropriately terse - i.e. `m.request(url)` - whereas `fetch` requires an additional explicit step to parse the response data as JSON: `fetch(url).then(function(response) {return response.json()})` The `fetch()` API does have a few technical advantages over `XMLHttpRequest` in a few uncommon cases: @@ -442,3 +465,24 @@ The `fetch()` API does have a few technical advantages over `XMLHttpRequest` in - it integrates to the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which provides an extra layer of control over how and when network requests happen. This API also allows access to push notifications and background synchronization features. In typical scenarios, streaming won't provide noticeable performance benefits because it's generally not advisable to download megabytes of data to begin with. Also, the memory gains from repeatedly reusing small buffers may be offset or nullified if they result in excessive browser repaints. For those reasons, choosing `fetch()` streaming instead of `m.request` is only recommended for extremely resource intensive applications. + +--- + +### Avoid anti-patterns + +#### Promises are not the response data + +The `m.request` method returns a [Promise](promise.md), not the response data itself. It cannot return that data directly because an HTTP request may take a long time to complete (due to network latency), and if Javascript waited for it, it would freeze the application until the data was available. + +```javascript +// AVOID +var users = m.request("/api/v1/users") +console.log("list of users:", users) +// `users` is NOT a list of users, it's a promise + +// PREFER +m.request("/api/v1/users").then(function(users) { + console.log("list of users:", users) +}) +``` + diff --git a/docs/route.md b/docs/route.md index 6bf16529..b16d1336 100644 --- a/docs/route.md +++ b/docs/route.md @@ -1,14 +1,15 @@ # route(root, defaultRoute, routes) -- [API](#api) -- [Static members](#static-members) - - [route.set](#routeset) - - [route.get](#routeget) - - [route.prefix](#routeprefix) - - [route.link](#routelink) -- [RouteResolver](#routeresolver) - - [routeResolver.onmatch](#routeresolveronmatch) - - [routeResolver.render](#routeresolverrender) +- [Description](#description) +- [Signature](#signature) + - [Static members](#static-members) + - [route.set](#routeset) + - [route.get](#routeget) + - [route.prefix](#routeprefix) + - [route.link](#routelink) + - [RouteResolver](#routeresolver) + - [routeResolver.onmatch](#routeresolveronmatch) + - [routeResolver.render](#routeresolverrender) - [How it works](#how-it-works) - [Typical usage](#typical-usage) - [Navigating to different routes](#navigating-to-different-routes) @@ -21,7 +22,25 @@ --- -### API +### Description + +Navigate between "pages" within an application + +```javascript +var Home = { + view: function() { + return "Welcome" + } +} + +m.route(document.body, "/home", { + "/home": Home, // defines `http://localhost/#!/home` +}) +``` + +--- + +### Signature `m.route(root, defaultRoute, routes)` diff --git a/docs/stream.md b/docs/stream.md index 3fc826d0..7b6110f8 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -1,17 +1,18 @@ # stream() -- [API](#api) -- [Static members](#static-members) - - [stream.combine](#streamcombine) - - [stream.merge](#streammerge) - - [stream.HALT](#streamhalt) - - [stream["fantasy-land/of"]](#streamfantasy-landof) -- [Instance members](#static-members) - - [stream.map](#streammap) - - [stream.end](#streamend) - - [stream["fantasy-land/of"]](#streamfantasy-landof) - - [stream["fantasy-land/map"]](#streamfantasy-landmap) - - [stream["fantasy-land/ap"]](#streamfantasy-landap) +- [Description](#description) +- [Signature](#signature) + - [Static members](#static-members) + - [stream.combine](#streamcombine) + - [stream.merge](#streammerge) + - [stream.HALT](#streamhalt) + - [stream["fantasy-land/of"]](#streamfantasy-landof) + - [Instance members](#static-members) + - [stream.map](#streammap) + - [stream.end](#streamend) + - [stream["fantasy-land/of"]](#streamfantasy-landof) + - [stream["fantasy-land/map"]](#streamfantasy-landmap) + - [stream["fantasy-land/ap"]](#streamfantasy-landap) - [Basic usage](#basic-usage) - [Streams as variables](#streams-as-variables) - [Bidirectional bindings](#bidirectional-bindings) @@ -25,7 +26,17 @@ --- -### API +### Description + +A Stream is a reactive data structure, similar to cells in spreadsheet applications. + +For example, in a spreadsheet, if `A1 = B1 + C1`, then changing the value of `B1` or `C1` automatically changes the value of `A1`. + +Similarly, you can make a stream depend on other streams so that changing the value of one automatically updates the other. This is useful when you have very expensive computations and want to only run them when necessary, as opposed to, say, on every redraw. + +--- + +### Signature Creates a stream diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 00000000..faef030c --- /dev/null +++ b/docs/style.css @@ -0,0 +1,45 @@ +body,table,h5 {font:normal 16px 'Open Sans';} +header,main {margin:auto;max-width:1000px;} +header section {position:absolute;width:250px;} +nav a {border-left:1px solid #ddd;padding:0 10px;} +nav a:first-child {border:0;padding-left:0;} +main {margin-bottom:100px;} +main section {margin-left:270px;} +h1 {margin:0 0 15px;} +h5 {font-style:italic;} +pre,code {background:#eee;font-family:monospace;} +pre {border-left:3px solid #1e5799;overflow:auto;padding:10px 20px;} +code {border:1px solid #ddd;display:inline-block;margin:0 0 1px;padding:3px;white-space:pre;} +pre code {border:0;margin:0;padding:0;} +table {border-collapse:collapse;width:100%;} +tbody tr:nth-child(odd) {background:#fafafa;} +thead tr,tbody tr:nth-child(even) {background:#f3f3f3;} +th {text-align:left;} +th,td {padding:3px 10px;vertical-align:top;} +a {color:#1e5799;display:inline-block;text-decoration:none;} +a:hover {text-decoration:underline;} +hr {border:0;border-bottom:1px solid #ddd;margin:30px 0;} + +#signature + p code {padding:3px 10px;} +h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;} +h1 + ul + hr {display:none;} +h1 + ul li {border-bottom:1px solid #eee;list-style:none;margin:0;padding:0;} +h1 + ul li:last-child {border-bottom:0;} +h1 + ul ul {margin:0 0 10px;padding:0 0 0 15px;} +h1 + ul ul li {border:0;} + +@media (max-width: 767px) { + main section {margin:0;} + h1 + ul + hr {display:block;} + header section,h1 + ul {margin:0 0 20px;position:static;width:auto;} +} +@media (max-width: 1024px) { + #signature + p + table,#signature + p + table tbody,#signature + p + table tr,#signature + p + table th,#signature + p + table td {display:block;} + #signature + p + table thead {display:none;} + #signature + p + table td:before {display:inline-block;font-style:italic;padding:0 10px 0 0;width:100px;} + #signature + p + table tr:not(:last-child) td:nth-child(1):before {content:"Argument:";} + #signature + p + table td:nth-child(2):before {content:"Type:";} + #signature + p + table td:nth-child(3):before {content:"Required:";} + #signature + p + table td:nth-child(4):before {content:"Description:";} + #signature + p + table tr:last-child td:nth-child(3) {display:none;} +} diff --git a/docs/testing.md b/docs/testing.md index cee5a78a..63f231cf 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,6 +1,6 @@ # Testing -Mithril comes with a testing framework called [ospec](../ospec/README.md). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). +Mithril comes with a testing framework called [ospec](https://github.com/lhorie/mithril.js/tree/rewrite/ospec). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). The easist way to setup the test runner is to create an NPM script for it. Open your project's `package.json` file and edit the `test` line under the `scripts` section: @@ -38,6 +38,8 @@ To run the test, use the command `npm test`. Ospec considers any Javascript file npm test ``` +--- + ### Good testing practices Generally speaking, there are two ways to write tests: upfront and after the fact. diff --git a/docs/trust.md b/docs/trust.md index efedf37a..d960e743 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -1,6 +1,7 @@ # trust(html) -- [API](#api) +- [Description](#description) +- [Signature](#signature) - [How it works](#how-it-works) - [Security considerations](#security-considerations) - [Scripts that do not run](#scripts-that-do-not-run) @@ -8,9 +9,15 @@ --- -### API +### Description -Generates a trusted HTML [vnode](vnodes.md) +Turns an HTML string into unescaped HTML. **Do not use `m.trust` on unsanitized user input.** + +Always try to use an [alternative method](#avoid-trusting-html) first, before considering using `m.trust`. + +--- + +### Signature `vnode = m.trust(html)` diff --git a/docs/version.md b/docs/version.md index 5cc6f219..683cd41e 100644 --- a/docs/version.md +++ b/docs/version.md @@ -1,11 +1,11 @@ # version -- [API](#api) +- [Signature](#signature) - [How it works](#how-it-works) --- -### API +### Signature `m.version` diff --git a/docs/vnodes.md b/docs/vnodes.md index 97b8d364..9fb23cde 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -16,7 +16,7 @@ The first time a virtual DOM tree is rendered, it is used as a blueprint to crea Typically, Virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes. -It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundres of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes. +It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundreds of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes. For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well. diff --git a/docs/withAttr.md b/docs/withAttr.md index aff44df9..10638e51 100644 --- a/docs/withAttr.md +++ b/docs/withAttr.md @@ -1,17 +1,38 @@ # withAttr(attrName, callback) -- [API](#api) -- [How to use](#how-to-use) +- [Description](#description) +- [Signature](#signature) +- [How it works](#how-it-works) - [Predictable event target](#predictable-event-target) - [Attributes and properties](#attributes-and-properties) --- -### API +### Description -Creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument. +Returns an event handler that runs `callback` with the value of the specified DOM attribute -This helper function is provided to help decouple the browser's event model from application code. +```javascript +var state = { + value: "", + setValue: function(v) {value = v} +} + +var Component = { + view: function() { + return m("input", { + oninput: m.withAttr("value", state.setValue), + value: state.value, + }) + } +} + +m.mount(document.body, Component) +``` + +--- + +### Signature `m.withAttr(attrName, callback, thisArg?)` @@ -26,7 +47,11 @@ Argument | Type | Required | Description --- -### How to use +### How it works + +The `m.withAttr` method creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument. + +This helper function is provided to help decouple the browser's event model from application code. ```javascript // standalone usage diff --git a/examples/dbmonster/angular/app.js b/examples/dbmonster/angular/app.js index 30ffd4f8..7e317be5 100644 --- a/examples/dbmonster/angular/app.js +++ b/examples/dbmonster/angular/app.js @@ -32,9 +32,10 @@ var AppComponent = ng.core.Component({selector: "my-app"}) this.update() }, update: function() { + requestAnimationFrame(function() {self.update()}) + var self = this self.databases = ENV.generateData().toArray() - setTimeout(function() {self.update()}, ENV.timeout) if (renderStage === 0) { renderStage = 1 diff --git a/examples/dbmonster/mithril-0.2.x/app.js b/examples/dbmonster/mithril-0.2.x/app.js deleted file mode 100644 index 56ea6006..00000000 --- a/examples/dbmonster/mithril-0.2.x/app.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict" - -var data = [] - -var root = document.getElementById("app") -update() - -function update() { - data = ENV.generateData().toArray() - - Monitoring.renderRate.ping() - - m.redraw(); - - setTimeout(update, ENV.timeout) -} - -m.mount(root, { - view : function() { - return m("div", [ - m("table", { className: "table table-striped latest-data" }, [ - m("tbody", - data.map(function(db) { - return m("tr", {key: db.dbname}, [ - m("td", { className: "dbname" }, db.dbname), - m("td", { className: "query-count" }, [ - m("span", { className: db.lastSample.countClassName }, db.lastSample.nbQueries) - ]), - db.lastSample.topFiveQueries.map(function(query) { - return m("td", { className: query.elapsedClassName }, [ - m("span", query.formatElapsed), - m("div", { className: "popover left" }, [ - m("div", { className: "popover-content" }, query.query), - m("div", { className: "arrow" }) - ]) - ]) - }) - ]) - }) - ) - ]) - ]) - } -}); diff --git a/examples/dbmonster/mithril-0.2.x/index.html b/examples/dbmonster/mithril-0.2.x/index.html deleted file mode 100644 index d6000ee0..00000000 --- a/examples/dbmonster/mithril-0.2.x/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - dbmon (Mithril 0.2.x) - - -
- - - - - - - diff --git a/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js b/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js deleted file mode 100644 index b98d933a..00000000 --- a/examples/dbmonster/mithril-0.2.x/mithril-0.2.4.js +++ /dev/null @@ -1,2190 +0,0 @@ -;(function (global, factory) { // eslint-disable-line - "use strict" - /* eslint-disable no-undef */ - var m = factory(global) - if (typeof module === "object" && module != null && module.exports) { - module.exports = m - } else if (typeof define === "function" && define.amd) { - define(function () { return m }) - } else { - global.m = m - } - /* eslint-enable no-undef */ -})(typeof window !== "undefined" ? window : this, function (global, undefined) { // eslint-disable-line - "use strict" - - m.version = function () { - return "v0.2.3" - } - - var hasOwn = {}.hasOwnProperty - var type = {}.toString - - function isFunction(object) { - return typeof object === "function" - } - - function isObject(object) { - return type.call(object) === "[object Object]" - } - - function isString(object) { - return type.call(object) === "[object String]" - } - - var isArray = Array.isArray || function (object) { - return type.call(object) === "[object Array]" - } - - function noop() {} - - var voidElements = { - AREA: 1, - BASE: 1, - BR: 1, - COL: 1, - COMMAND: 1, - EMBED: 1, - HR: 1, - IMG: 1, - INPUT: 1, - KEYGEN: 1, - LINK: 1, - META: 1, - PARAM: 1, - SOURCE: 1, - TRACK: 1, - WBR: 1 - } - - // caching commonly used variables - var $document, $location, $requestAnimationFrame, $cancelAnimationFrame - - // self invoking function needed because of the way mocks work - function initialize(mock) { - $document = mock.document - $location = mock.location - $cancelAnimationFrame = mock.cancelAnimationFrame || mock.clearTimeout - $requestAnimationFrame = mock.requestAnimationFrame || mock.setTimeout - } - - // testing API - m.deps = function (mock) { - initialize(global = mock || window) - return global - } - - m.deps(global) - - /** - * @typedef {String} Tag - * A string that looks like -> div.classname#id[param=one][param2=two] - * Which describes a DOM node - */ - - function parseTagAttrs(cell, tag) { - var classes = [] - var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g - var match - - while ((match = parser.exec(tag))) { - if (match[1] === "" && match[2]) { - cell.tag = match[2] - } else if (match[1] === "#") { - cell.attrs.id = match[2] - } else if (match[1] === ".") { - classes.push(match[2]) - } else if (match[3][0] === "[") { - var pair = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3]) - cell.attrs[pair[1]] = pair[3] || "" - } - } - - return classes - } - - function getVirtualChildren(args, hasAttrs) { - var children = hasAttrs ? args.slice(1) : args - - if (children.length === 1 && isArray(children[0])) { - return children[0] - } else { - return children - } - } - - function assignAttrs(target, attrs, classes) { - var classAttr = "class" in attrs ? "class" : "className" - - for (var attrName in attrs) { - if (hasOwn.call(attrs, attrName)) { - if (attrName === classAttr && - attrs[attrName] != null && - attrs[attrName] !== "") { - classes.push(attrs[attrName]) - // create key in correct iteration order - target[attrName] = "" - } else { - target[attrName] = attrs[attrName] - } - } - } - - if (classes.length) target[classAttr] = classes.join(" ") - } - - /** - * - * @param {Tag} The DOM node tag - * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs - * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, - * or splat (optional) - */ - function m(tag, pairs) { - var args = [] - - for (var i = 1, length = arguments.length; i < length; i++) { - args[i - 1] = arguments[i] - } - - if (isObject(tag)) return parameterize(tag, args) - - if (!isString(tag)) { - throw new Error("selector in m(selector, attrs, children) should " + - "be a string") - } - - var hasAttrs = pairs != null && isObject(pairs) && - !("tag" in pairs || "view" in pairs || "subtree" in pairs) - - var attrs = hasAttrs ? pairs : {} - var cell = { - tag: "div", - attrs: {}, - children: getVirtualChildren(args, hasAttrs) - } - - assignAttrs(cell.attrs, attrs, parseTagAttrs(cell, tag)) - return cell - } - - function forEach(list, f) { - for (var i = 0; i < list.length && !f(list[i], i++);) { - // function called in condition - } - } - - function forKeys(list, f) { - forEach(list, function (attrs, i) { - return (attrs = attrs && attrs.attrs) && - attrs.key != null && - f(attrs, i) - }) - } - // This function was causing deopts in Chrome. - function dataToString(data) { - // data.toString() might throw or return null if data is the return - // value of Console.log in some versions of Firefox (behavior depends on - // version) - try { - if (data != null && data.toString() != null) return data - } catch (e) { - // silently ignore errors - } - return "" - } - - // This function was causing deopts in Chrome. - function injectTextNode(parentElement, first, index, data) { - try { - insertNode(parentElement, first, index) - first.nodeValue = data - } catch (e) { - // IE erroneously throws error when appending an empty text node - // after a null - } - } - - function flatten(list) { - // recursively flatten array - for (var i = 0; i < list.length; i++) { - if (isArray(list[i])) { - list = list.concat.apply([], list) - // check current index again and flatten until there are no more - // nested arrays at that index - i-- - } - } - return list - } - - function insertNode(parentElement, node, index) { - parentElement.insertBefore(node, - parentElement.childNodes[index] || null) - } - - var DELETION = 1 - var INSERTION = 2 - var MOVE = 3 - - function handleKeysDiffer(data, existing, cached, parentElement) { - forKeys(data, function (key, i) { - existing[key = key.key] = existing[key] ? { - action: MOVE, - index: i, - from: existing[key].index, - element: cached.nodes[existing[key].index] || - $document.createElement("div") - } : {action: INSERTION, index: i} - }) - - var actions = [] - for (var prop in existing) { - if (hasOwn.call(existing, prop)) { - actions.push(existing[prop]) - } - } - - var changes = actions.sort(sortChanges) - var newCached = new Array(cached.length) - - newCached.nodes = cached.nodes.slice() - - forEach(changes, function (change) { - var index = change.index - if (change.action === DELETION) { - clear(cached[index].nodes, cached[index]) - newCached.splice(index, 1) - } - if (change.action === INSERTION) { - var dummy = $document.createElement("div") - dummy.key = data[index].attrs.key - insertNode(parentElement, dummy, index) - newCached.splice(index, 0, { - attrs: {key: data[index].attrs.key}, - nodes: [dummy] - }) - newCached.nodes[index] = dummy - } - - if (change.action === MOVE) { - var changeElement = change.element - var maybeChanged = parentElement.childNodes[index] - if (maybeChanged !== changeElement && changeElement !== null) { - parentElement.insertBefore(changeElement, - maybeChanged || null) - } - newCached[index] = cached[change.from] - newCached.nodes[index] = changeElement - } - }) - - return newCached - } - - function diffKeys(data, cached, existing, parentElement) { - var keysDiffer = data.length !== cached.length - - if (!keysDiffer) { - forKeys(data, function (attrs, i) { - var cachedCell = cached[i] - return keysDiffer = cachedCell && - cachedCell.attrs && - cachedCell.attrs.key !== attrs.key - }) - } - - if (keysDiffer) { - return handleKeysDiffer(data, existing, cached, parentElement) - } else { - return cached - } - } - - function diffArray(data, cached, nodes) { - // diff the array itself - - // update the list of DOM nodes by collecting the nodes from each item - forEach(data, function (_, i) { - if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) - }) - // remove items from the end of the array if the new array is shorter - // than the old one. if errors ever happen here, the issue is most - // likely a bug in the construction of the `cached` data structure - // somewhere earlier in the program - forEach(cached.nodes, function (node, i) { - if (node.parentNode != null && nodes.indexOf(node) < 0) { - clear([node], [cached[i]]) - } - }) - - if (data.length < cached.length) cached.length = data.length - cached.nodes = nodes - } - - function buildArrayKeys(data) { - var guid = 0 - forKeys(data, function () { - forEach(data, function (attrs) { - if ((attrs = attrs && attrs.attrs) && attrs.key == null) { - attrs.key = "__mithril__" + guid++ - } - }) - return 1 - }) - } - - function isDifferentEnough(data, cached, dataAttrKeys) { - if (data.tag !== cached.tag) return true - - if (dataAttrKeys.sort().join() !== - Object.keys(cached.attrs).sort().join()) { - return true - } - - if (data.attrs.id !== cached.attrs.id) { - return true - } - - if (data.attrs.key !== cached.attrs.key) { - return true - } - - if (m.redraw.strategy() === "all") { - return !cached.configContext || cached.configContext.retain !== true - } - - if (m.redraw.strategy() === "diff") { - return cached.configContext && cached.configContext.retain === false - } - - return false - } - - function maybeRecreateObject(data, cached, dataAttrKeys) { - // if an element is different enough from the one in cache, recreate it - if (isDifferentEnough(data, cached, dataAttrKeys)) { - if (cached.nodes.length) clear(cached.nodes) - - if (cached.configContext && - isFunction(cached.configContext.onunload)) { - cached.configContext.onunload() - } - - if (cached.controllers) { - forEach(cached.controllers, function (controller) { - if (controller.onunload) { - controller.onunload({preventDefault: noop}) - } - }) - } - } - } - - function getObjectNamespace(data, namespace) { - if (data.attrs.xmlns) return data.attrs.xmlns - if (data.tag === "svg") return "http://www.w3.org/2000/svg" - if (data.tag === "math") return "http://www.w3.org/1998/Math/MathML" - return namespace - } - - var pendingRequests = 0 - m.startComputation = function () { pendingRequests++ } - m.endComputation = function () { - if (pendingRequests > 1) { - pendingRequests-- - } else { - pendingRequests = 0 - m.redraw() - } - } - - function unloadCachedControllers(cached, views, controllers) { - if (controllers.length) { - cached.views = views - cached.controllers = controllers - forEach(controllers, function (controller) { - if (controller.onunload && controller.onunload.$old) { - controller.onunload = controller.onunload.$old - } - - if (pendingRequests && controller.onunload) { - var onunload = controller.onunload - controller.onunload = noop - controller.onunload.$old = onunload - } - }) - } - } - - function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { - // schedule configs to be called. They are called after `build` finishes - // running - if (isFunction(data.attrs.config)) { - var context = cached.configContext = cached.configContext || {} - - // bind - configs.push(function () { - return data.attrs.config.call(data, node, !isNew, context, - cached) - }) - } - } - - function buildUpdatedNode( - cached, - data, - editable, - hasKeys, - namespace, - views, - configs, - controllers - ) { - var node = cached.nodes[0] - - if (hasKeys) { - setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) - } - - cached.children = build( - node, - data.tag, - undefined, - undefined, - data.children, - cached.children, - false, - 0, - data.attrs.contenteditable ? node : editable, - namespace, - configs - ) - - cached.nodes.intact = true - - if (controllers.length) { - cached.views = views - cached.controllers = controllers - } - - return node - } - - function handleNonexistentNodes(data, parentElement, index) { - var nodes - if (data.$trusted) { - nodes = injectHTML(parentElement, index, data) - } else { - nodes = [$document.createTextNode(data)] - if (!(parentElement.nodeName in voidElements)) { - insertNode(parentElement, nodes[0], index) - } - } - - var cached - - if (typeof data === "string" || - typeof data === "number" || - typeof data === "boolean") { - cached = new data.constructor(data) - } else { - cached = data - } - - cached.nodes = nodes - return cached - } - - function reattachNodes( - data, - cached, - parentElement, - editable, - index, - parentTag - ) { - var nodes = cached.nodes - if (!editable || editable !== $document.activeElement) { - if (data.$trusted) { - clear(nodes, cached) - nodes = injectHTML(parentElement, index, data) - } else if (parentTag === "textarea") { - //