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(/
+```
+
+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
+
+
+
+
+
+
+
+