diff --git a/.eslintignore b/.eslintignore
index a6ff032f..0b24c5c3 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,5 @@
# Most of these are build artifacts.
-node_modules
+**/node_modules
**/*.min.js
archive
deploy
diff --git a/bench/README.md b/bench/README.md
new file mode 100644
index 00000000..16fcc8b9
--- /dev/null
+++ b/bench/README.md
@@ -0,0 +1,4 @@
+This suite is taken with modifications from the Mithril example in
+[Merri/nom](https://github.com/Merri/nom/tree/gh-pages/todomvc/benchmark/mithril-new).
+
+(For example, it uses Mithril itself to help simplify the business logic.)
diff --git a/bench/app/.gitignore b/bench/app/.gitignore
new file mode 100644
index 00000000..11c90f9c
--- /dev/null
+++ b/bench/app/.gitignore
@@ -0,0 +1,6 @@
+node_modules/todomvc-app-css/*
+!node_modules/todomvc-app-css/index.css
+
+node_modules/todomvc-common/*
+!node_modules/todomvc-common/base.js
+!node_modules/todomvc-common/base.css
diff --git a/bench/app/index.html b/bench/app/index.html
new file mode 100644
index 00000000..e42cfacf
--- /dev/null
+++ b/bench/app/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+ Mithril • TodoMVC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bench/app/js/app.js b/bench/app/js/app.js
new file mode 100644
index 00000000..d4a8e6fb
--- /dev/null
+++ b/bench/app/js/app.js
@@ -0,0 +1,13 @@
+/* global m */
+(function (app) {
+ "use strict"
+
+ app.ENTER_KEY = 13
+ app.ESC_KEY = 27
+
+ m.route.mode = "hash"
+ m.route(document.getElementById("todoapp"), "/", {
+ "/": app,
+ "/:filter": app
+ })
+})(this.app || (this.app = {}))
diff --git a/bench/app/js/controllers/todo.js b/bench/app/js/controllers/todo.js
new file mode 100644
index 00000000..27577689
--- /dev/null
+++ b/bench/app/js/controllers/todo.js
@@ -0,0 +1,111 @@
+/* global m */
+(function (app) {
+ "use strict"
+
+ app.controller = function () {
+ /* eslint-disable no-warning-comments */
+ // Todo collection, update with props
+ /* eslint-enable no-warning-comments */
+ this.list = app.storage.get().map(function (item) {
+ return new app.Todo(item)
+ })
+
+ // Temp title placeholder
+ this.title = m.prop("")
+
+ /* eslint-disable no-warning-comments */
+ // Todo list filter
+ /* eslint-enable no-warning-comments */
+ this.filter = m.prop(m.route.param("filter") || "")
+
+ this.add = function () {
+ var title = this.title().trim()
+ if (title) {
+ this.list.push(new app.Todo({title: title}))
+ app.storage.put(this.list)
+ }
+ this.title("")
+ }
+
+ this.isVisible = function (todo) {
+ switch (this.filter()) {
+ case "active": return !todo.completed()
+ case "completed": return todo.completed()
+ default: return true
+ }
+ }
+
+ this.complete = function (todo) {
+ if (todo.completed()) {
+ todo.completed(false)
+ } else {
+ todo.completed(true)
+ }
+ app.storage.put(this.list)
+ }
+
+ this.edit = function (todo) {
+ todo.previousTitle = todo.title()
+ todo.editing(true)
+ }
+
+ this.doneEditing = function (todo, index) {
+ todo.editing(false)
+ todo.title(todo.title().trim())
+ if (!todo.title()) {
+ this.list.splice(index, 1)
+ }
+ app.storage.put(this.list)
+ }
+
+ this.cancelEditing = function (todo) {
+ todo.title(todo.previousTitle)
+ todo.editing(false)
+ }
+
+ this.clearTitle = function () {
+ this.title("")
+ }
+
+ this.remove = function (key) {
+ this.list.splice(key, 1)
+ app.storage.put(this.list)
+ }
+
+ this.clearCompleted = function () {
+ for (var i = this.list.length - 1; i >= 0; i--) {
+ if (this.list[i].completed()) {
+ this.list.splice(i, 1)
+ }
+ }
+ app.storage.put(this.list)
+ }
+
+ this.amountCompleted = function () {
+ var amount = 0
+ for (var i = 0; i < this.list.length; i++) {
+ if (this.list[i].completed()) {
+ amount++
+ }
+ }
+ return amount
+ }
+
+ this.allCompleted = function () {
+ for (var i = 0; i < this.list.length; i++) {
+ if (!this.list[i].completed()) {
+ return false
+ }
+ }
+ return true
+ }
+
+ this.completeAll = function () {
+ var allCompleted = this.allCompleted()
+ for (var i = 0; i < this.list.length; i++) {
+ this.list[i].completed(!allCompleted)
+ }
+ app.storage.put(this.list)
+ }
+ }
+})(this.app || (this.app = {}))
diff --git a/bench/app/js/models/storage.js b/bench/app/js/models/storage.js
new file mode 100644
index 00000000..0e867e9a
--- /dev/null
+++ b/bench/app/js/models/storage.js
@@ -0,0 +1,14 @@
+(function (app) {
+ "use strict"
+
+ var STORAGE_ID = "todos-mithril"
+
+ app.storage = {
+ get: function () {
+ return JSON.parse(localStorage.getItem(STORAGE_ID) || "[]")
+ },
+ put: function (todos) {
+ localStorage.setItem(STORAGE_ID, JSON.stringify(todos))
+ }
+ }
+})(this.app || (this.app = {}))
diff --git a/bench/app/js/models/todo.js b/bench/app/js/models/todo.js
new file mode 100644
index 00000000..77af9ba2
--- /dev/null
+++ b/bench/app/js/models/todo.js
@@ -0,0 +1,12 @@
+/* global m */
+
+(function (app) {
+ "use strict"
+
+ // Todo Model
+ app.Todo = function (data) {
+ this.title = m.prop(data.title)
+ this.completed = m.prop(data.completed || false)
+ this.editing = m.prop(data.editing || false)
+ }
+})(this.app || (this.app = {}))
diff --git a/bench/app/js/views/footer-view.js b/bench/app/js/views/footer-view.js
new file mode 100644
index 00000000..25f07f1c
--- /dev/null
+++ b/bench/app/js/views/footer-view.js
@@ -0,0 +1,40 @@
+/* global m */
+(function (app) {
+ "use strict"
+
+ var filter = {
+ view: function (_, ctrl, expected, name, href) {
+ return m("li", [
+ m("a", {
+ href: href,
+ config: m.route,
+ class: ctrl.filter() === expected ? "selected" : ""
+ }, name)
+ ])
+ }
+ }
+
+ app.footer = {
+ view: function (_, ctrl) {
+ var amountCompleted = ctrl.amountCompleted()
+ var amountActive = ctrl.list.length - amountCompleted
+
+ return m("footer#footer", [
+ m("span#todo-count", [
+ m("strong", amountActive), " item" +
+ (amountActive !== 1 ? "s" : "") + " left"
+ ]),
+ m("ul#filters", [
+ m(filter, ctrl, "", "All", "/"),
+ m(filter, ctrl, "active", "Active", "/active"),
+ m(filter, ctrl, "completed", "Completed", "/completed")
+ ]),
+ ctrl.amountCompleted() ? m("button#clear-completed", {
+ onclick: function () {
+ ctrl.clearCompleted()
+ }
+ }, "Clear completed") : null
+ ])
+ }
+ }
+})(this.app || (this.app = {}))
diff --git a/bench/app/js/views/main-view.js b/bench/app/js/views/main-view.js
new file mode 100644
index 00000000..9dfd2667
--- /dev/null
+++ b/bench/app/js/views/main-view.js
@@ -0,0 +1,119 @@
+/* global m */
+(function (app) {
+ "use strict"
+
+ // View utility
+ app.watchInput = function (onenter, onescape) {
+ return function (e) {
+ if (e.keyCode === app.ENTER_KEY) {
+ onenter()
+ } else if (e.keyCode === app.ESC_KEY) {
+ onescape()
+ }
+ }
+ }
+
+ var header = {
+ controller: function () {
+ this.focused = false
+ },
+
+ view: function (ctrl, parentCtrl) {
+ return m("header#header", [
+ m("h1", "todos"),
+ m('input#new-todo[placeholder="What needs to be done?"]', {
+ onkeyup: app.watchInput(
+ function () {
+ parentCtrl.add()
+ },
+ function () {
+ parentCtrl.clearTitle()
+ }),
+ value: parentCtrl.title(),
+ oninput: m.withAttr("value", parentCtrl.title),
+ config: function (element) {
+ if (!ctrl.focused) {
+ element.focus()
+ ctrl.focused = true
+ }
+ }
+ })
+ ])
+ }
+ }
+
+ var todo = {
+ view: function (_, parentCtrl, task, index) {
+ return m("li", {
+ class: (task.completed() ? "completed" : "") +
+ (task.editing() ? " editing" : "")
+ }, [
+ m(".view", [
+ m("input.toggle[type=checkbox]", {
+ onclick: m.withAttr("checked", function () {
+ parentCtrl.complete(task)
+ }),
+ checked: task.completed()
+ }),
+ m("label", {
+ ondblclick: function () {
+ parentCtrl.edit(task)
+ }
+ }, task.title()),
+ m("button.destroy", {
+ onclick: function () {
+ parentCtrl.remove(index)
+ }
+ })
+ ]),
+ m("input.edit", {
+ value: task.title(),
+ onkeyup: app.watchInput(
+ function () {
+ parentCtrl.doneEditing(task, index)
+ },
+ function () {
+ parentCtrl.cancelEditing(task)
+ }),
+ oninput: m.withAttr("value", task.title),
+ config: function (element) {
+ if (task.editing()) {
+ element.focus()
+ element.selectionStart =
+ element.value.length
+ }
+ },
+ onblur: function () {
+ parentCtrl.doneEditing(task, index)
+ }
+ })
+ ])
+ }
+ }
+
+ app.view = function (ctrl) {
+ return m("div", [
+ m(header, ctrl),
+ m("section#main", {
+ style: {
+ display: ctrl.list.length ? "" : "none"
+ }
+ }, [
+ m("input#toggle-all[type=checkbox]", {
+ onclick: ctrl.completeAll.bind(ctrl),
+ checked: ctrl.allCompleted()
+ }),
+ m("ul#todo-list", [
+ ctrl.list
+ .filter(function () {
+ return ctrl.isVisible()
+ })
+ .map(function (task, index) {
+ return m(todo, ctrl, task, index)
+ })
+ ])
+ ]),
+ ctrl.list.length === 0 ? "" : m(app.footer, ctrl)
+ ])
+ }
+})(this.app || (this.app = {}))
diff --git a/bench/app/package.json b/bench/app/package.json
new file mode 100644
index 00000000..3b1e70fd
--- /dev/null
+++ b/bench/app/package.json
@@ -0,0 +1,7 @@
+{
+ "private": true,
+ "dependencies": {
+ "todomvc-common": "^1.0.1",
+ "todomvc-app-css": "^1.0.1"
+ }
+}
diff --git a/bench/app/readme.md b/bench/app/readme.md
new file mode 100644
index 00000000..76a71d43
--- /dev/null
+++ b/bench/app/readme.md
@@ -0,0 +1,18 @@
+# Mithril TodoMVC Example
+
+> [Mithril](http://lhorie.github.io/mithril/) is a client-side MVC framework - a tool to organize code in a way that is easy to think about and to maintain.
+
+> _[Mithril - lhorie.github.io/mithril/](http://lhorie.github.io/mithril/)_
+
+## Learning Mithril
+
+The [Mithril website](http://lhorie.github.io/mithril/getting-started.html) is a great resource for getting started.
+
+Here are some links you may find helpful:
+
+* [Official Documentation](http://lhorie.github.io/mithril/mithril.html)
+
+_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
+
+## Credit
+This TodoMVC application was created by [taylorhakes](https://github.com/taylorhakes).
\ No newline at end of file
diff --git a/bench/index.html b/bench/index.html
new file mode 100644
index 00000000..6f399b3d
--- /dev/null
+++ b/bench/index.html
@@ -0,0 +1,81 @@
+
+TodoMVC Benchmark
+
+
+
+
+
+
+
+
diff --git a/bench/resources/benchmark-runner.js b/bench/resources/benchmark-runner.js
new file mode 100644
index 00000000..e3d2e908
--- /dev/null
+++ b/bench/resources/benchmark-runner.js
@@ -0,0 +1,268 @@
+(function (global) { // eslint-disable-line max-statements
+ "use strict"
+ var m = global.m
+
+ window.onhashchange = function () {
+ location.reload()
+ }
+
+ global.BenchmarkTestStep = BenchmarkTestStep
+ function BenchmarkTestStep(name, run) {
+ this.name = name
+ this.run = run
+ }
+
+ global.BenchmarkRunner = BenchmarkRunner
+ function BenchmarkRunner(suites, client) {
+ this._suites = suites
+ this._prepareReturnValue = null
+ this._measuredValues = {}
+ this._client = client
+ }
+
+ BenchmarkRunner.prototype.waitForElement = function (selector) {
+ var deferred = m.deferred()
+ var contentDocument = this._frame.contentDocument
+
+ function resolveIfReady() {
+ var element = contentDocument.querySelector(selector)
+ if (element) {
+ return deferred.resolve(element)
+ }
+ setTimeout(resolveIfReady, 50)
+ }
+
+ resolveIfReady()
+ return deferred.promise
+ }
+
+ BenchmarkRunner.prototype._removeFrame = function () {
+ if (this._frame) {
+ this._frame.parentNode.removeChild(this._frame)
+ this._frame = null
+ }
+ }
+
+ BenchmarkRunner.prototype._appendFrame = function () {
+ var frame = document.createElement("iframe")
+ frame.style.width = "800px"
+ frame.style.height = "600px"
+ document.body.appendChild(frame)
+ this._frame = frame
+ return frame
+ }
+
+ BenchmarkRunner.prototype._waitAndWarmUp = function () {
+ var startTime = Date.now()
+
+ function Fibonacci(n) {
+ if (Date.now() - startTime > 100) return
+ else if (n <= 0) return 0
+ else if (n === 1) return 1
+ else return Fibonacci(n - 2) + Fibonacci(n - 1)
+ }
+
+ var deferred = m.deferred()
+ setTimeout(function () {
+ Fibonacci(100)
+ deferred.resolve()
+ }, 200)
+ return deferred.promise
+ }
+
+ var now = window.performance && window.performance.now ?
+ function () { return window.performance.now() } :
+ function () { return +new Date() }
+
+ function logResults(document, expected, suite, test, callback) {
+ var count = document.querySelectorAll(".view").length
+ if (count !== expected) {
+ console.error([ // eslint-disable-line no-console
+ suite.name,
+ test.name,
+ "expected",
+ expected,
+ "got",
+ count
+ ])
+ callback(NaN, NaN)
+ }
+ }
+
+ // This function must be as simple as possible, so it doesn't interfere
+ // with reading the times
+ BenchmarkRunner.prototype._runTest = function (
+ suite,
+ test,
+ prepareReturnValue,
+ callback
+ ) {
+ var testFunction = test.run
+
+ var window = this._frame.contentWindow
+ var document = this._frame.contentDocument
+ var expected = this._client.numberOfItemsToAdd
+
+ var startTime = now()
+ testFunction(prepareReturnValue, window, document)
+ var endTime = now()
+ var syncTime = endTime - startTime
+
+ startTime = now()
+ setTimeout(function () {
+ setTimeout(function () {
+ var endTime = now()
+
+ // if the DOM count is wrong after a test, don't report its
+ // results.
+ if (/Adding|Completing/.test(test.name)) {
+ logResults(document, expected, suite, test, callback)
+ }
+
+ if (/Deleting/.test(test.name)) {
+ logResults(document, 0, suite, test, callback)
+ }
+
+ callback(syncTime, endTime - startTime)
+ }, 0)
+ }, 0)
+ }
+
+ function BenchmarkState(suites) {
+ this._suites = suites
+ this._suiteIndex = -1
+ this._testIndex = 0
+ this.next()
+ }
+
+ BenchmarkState.prototype.currentSuite = function () {
+ return this._suites[this._suiteIndex]
+ }
+
+ BenchmarkState.prototype.currentTest = function () {
+ var suite = this.currentSuite()
+ return suite ? suite.tests[this._testIndex] : null
+ }
+
+ BenchmarkState.prototype.next = function () {
+ this._testIndex++
+
+ var suite = this._suites[this._suiteIndex]
+ if (suite && this._testIndex < suite.tests.length) {
+ return this
+ }
+
+ this._testIndex = 0
+
+ var i = this._suiteIndex
+ var suites = this._suites
+
+ do {
+ i++
+ } while (i < suites.length && suites[i].disabled)
+
+ this._suiteIndex = i
+
+ return this
+ }
+
+ BenchmarkState.prototype.isFirstTest = function () {
+ return !this._testIndex
+ }
+
+ BenchmarkState.prototype.prepareCurrentSuite = function (runner, frame) {
+ var suite = this.currentSuite()
+ var deferred = m.deferred()
+ frame.onload = function () {
+ suite.prepare(runner, frame.contentWindow, frame.contentDocument)
+ .then(function (result) {
+ deferred.resolve(result)
+ }, function (err) {
+ deferred.reject(err)
+ })
+ }
+ frame.src = suite.url
+ return deferred.promise
+ }
+
+ BenchmarkRunner.prototype.step = function (state) {
+ if (!state) {
+ state = new BenchmarkState(this._suites)
+ }
+
+ var suite = state.currentSuite()
+ if (!suite) {
+ this._finalize()
+ var deferred = m.deferred()
+ deferred.resolve()
+ return deferred.promise
+ }
+
+ if (state.isFirstTest()) {
+ this._masuredValuesForCurrentSuite = {}
+ var self = this
+ return state.prepareCurrentSuite(this, this._appendFrame())
+ .then(function (prepareReturnValue) {
+ self._prepareReturnValue = prepareReturnValue
+ return self._runTestAndRecordResults(state)
+ })
+ }
+
+ return this._runTestAndRecordResults(state)
+ }
+
+ BenchmarkRunner.prototype._runTestAndRecordResults = function (state) {
+ var deferred = m.deferred()
+ var suite = state.currentSuite()
+ var test = state.currentTest()
+
+ if (this._client && this._client.willRunTest) {
+ this._client.willRunTest(suite, test)
+ }
+
+ var self = this
+ setTimeout(function () {
+ self._runTest(suite, test, self._prepareReturnValue,
+ function (syncTime, asyncTime) {
+ var suiteResults
+ if (self._measuredValues[suite.name]) {
+ suiteResults = self._measuredValues[suite.name]
+ } else {
+ suiteResults = {tests: {}, total: 0}
+ }
+
+ self._measuredValues[suite.name] = suiteResults
+
+ suiteResults.tests[test.name] = {
+ Sync: syncTime,
+ Async: asyncTime
+ }
+
+ suiteResults.total += syncTime + asyncTime
+
+ if (self._client && self._client.willRunTest) {
+ self._client.didRunTest(suite, test)
+ }
+
+ state.next()
+ if (state.currentSuite() !== suite) {
+ self._removeFrame()
+ }
+
+ deferred.resolve(state)
+ })
+ }, 0)
+ return deferred.promise
+ }
+
+ BenchmarkRunner.prototype._finalize = function () {
+ this._removeFrame()
+
+ if (this._client && this._client.didRunSuites) {
+ this._client.didRunSuites(this._measuredValues)
+ }
+
+ // FIXME: This should be done when we start running tests.
+ this._measuredValues = {}
+ }
+})(this)
diff --git a/bench/resources/manager.js b/bench/resources/manager.js
new file mode 100644
index 00000000..2b422792
--- /dev/null
+++ b/bench/resources/manager.js
@@ -0,0 +1,237 @@
+/* global m */
+/* eslint no-console: 0 */
+
+(function (global) { // eslint-disable-line max-statements
+ "use strict"
+
+ var numberOfItemsToAdd = (~~location.hash.slice(1)) || 250
+ var runs = []
+ var timesRan = 0
+ var runButton
+
+ function forOwn(object, f) {
+ for (var key in object) if ({}.hasOwnProperty.call(object, key)) {
+ f(object[key], key)
+ }
+ }
+
+ var append = Function.call.bind(function append() {
+ /* eslint-disable no-invalid-this */
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i]
+ if (Array.isArray(arg)) append.apply(this, arg)
+ else this.appendChild(arg)
+ }
+ return this
+ /* eslint-enable no-invalid-this */
+ })
+
+ function n(type, attrs) {
+ var el = document.createElement(type)
+ if (!attrs) return el
+ forOwn(attrs, function (value, attr) {
+ if (attr === "style") {
+ forOwn(value, function (k, v) { el.style[k] = v })
+ } else {
+ el[attr] = value
+ }
+ })
+ return el
+ }
+
+ function createButton(text, onclick) {
+ return n("button", {textContent: text, onclick: onclick})
+ }
+
+ function createTest(suite, test) {
+ return append(n("li"),
+ append(test.anchor = n("a", {id: suite.name + "-" + test.name}),
+ document.createTextNode(suite.name + "/" + test.name)))
+ }
+
+ function createSuiteCheckbox(suite) {
+ return n("input", {
+ id: suite.name,
+ type: "checkbox",
+ checked: true,
+ onchange: function () {
+ suite.disabled = !this.checked
+ }
+ })
+ }
+
+ function createSuiteLabel(suite) {
+ return append(n("label", {htmlFor: suite.name}),
+ document.createTextNode(suite.name + " " + suite.version))
+ }
+
+ function createSuite(suite) {
+ return append(n("li"),
+ createSuiteCheckbox(suite),
+ createSuiteLabel(suite),
+ append(n("ol"), suite.tests.map(createTest.bind(null, suite))))
+ }
+
+ function createUIForSuites(suites, onstep, onrun) {
+ return append(n("nav"),
+ createButton("Step Tests", onstep),
+ runButton = createButton("Run All", onrun),
+ append(n("ol"), suites.map(createSuite)))
+ }
+
+ function generateResults(measured, timesToRun) {
+ var result = ""
+ var total = 0 // FIXME: Compute the total properly.
+
+ forOwn(measured, function (suiteResults, suite) {
+ forOwn(suiteResults.tests, function (testResults, test) {
+ forOwn(testResults, function (subtestResults, subtest) {
+ result += suite + " : " + test + " : " + subtest + ": " +
+ subtestResults + " ms\n"
+ })
+ })
+ result += suite + " : " + suiteResults.total + " ms\n"
+ total += suiteResults.total
+ })
+ return result + "Run " + (runs.length + 1) + "/" + timesToRun +
+ " - Total : " + total + " ms\n"
+ }
+
+ function reportFastest() {
+ var results = {}
+ runs.forEach(function (runData) {
+ forOwn(runData, function (data, key) {
+ results[key] = Math.min(
+ results[key] || Infinity,
+ data.total
+ )
+ })
+ })
+ return results
+ }
+
+ global.google.load("visualization", "1", {packages: ["corechart"]})
+ function drawChart(results) {
+ var V = global.google.visualization
+
+ var raw = []
+ forOwn(results, function (result, key) {
+ raw.push([key, Math.round(result), colorify(key)])
+ })
+ raw.sort(function (a, b) { return a[1] - b[1] })
+ raw.unshift(["Project", "Time", {role: "style"}])
+
+ var runWord = "run" + (runs.length > 1 ? "s" : "")
+ var title = "Best time in milliseconds over " + runs.length + " " +
+ runWord + " (lower is better)"
+
+ var view = new V.DataView(V.arrayToDataTable(raw))
+ view.setColumns([0, 1, {
+ calc: "stringify",
+ sourceColumn: 1,
+ type: "string",
+ role: "annotation"
+ }, 2])
+
+ document.getElementById("analysis").style.display = "block"
+
+ new V.BarChart(document.getElementById("barchart-values")).draw(view, {
+ title: "TodoMVC Benchmark",
+ width: 600,
+ height: 400,
+ legend: {position: "none"},
+ backgroundColor: "transparent",
+ hAxis: {title: title},
+ min: 0,
+ max: 1500
+ })
+ }
+
+ function colorPart(n, pre) {
+ return Math.max(
+ 0,
+ ((n.toLowerCase().charCodeAt(pre % n.length) - 97) / 26 * 255) | 0
+ )
+ }
+
+ function colorify(n) {
+ return "rgb(" +
+ colorPart(n, 3) + ", " +
+ colorPart(n, 4) + ", " +
+ colorPart(n, 5) + ")"
+ }
+
+ function shuffle(ary) {
+ for (var i = 0; i < ary.length; i++) {
+ var j = Math.floor(Math.random() * (i + 1))
+ var tmp = ary[i]
+ ary[i] = ary[j]
+ ary[j] = tmp
+ }
+ }
+
+ window.addEventListener("load", function () {
+ var match = window.location.search.match(/[\?&]r=(\d+)/)
+ var timesToRun = match ? +(match[1]) : 1
+
+ var Suites = global.Suites.map(function (suite) {
+ suite = suite(numberOfItemsToAdd)
+ suite.disabled = false
+ return suite
+ })
+
+ var runner = new global.BenchmarkRunner(Suites, {
+ numberOfItemsToAdd: numberOfItemsToAdd,
+
+ willRunTest: function (suite, test) {
+ if (test.anchor.classList) {
+ test.anchor.classList.add("running")
+ }
+ },
+
+ didRunTest: function (suite, test) {
+ if (test.anchor.classList) {
+ test.anchor.classList.remove("running")
+ test.anchor.classList.add("ran")
+ }
+ },
+
+ didRunSuites: function (measured) {
+ var results = generateResults(measured, timesToRun)
+ if (results) {
+ console.log(results)
+
+ runs.push(measured)
+ timesRan++
+ if (timesRan >= timesToRun) {
+ timesRan = 0
+ drawChart(reportFastest())
+ shuffle(Suites)
+ } else {
+ setTimeout(function () {
+ runButton.click()
+ }, 0)
+ }
+ }
+ }
+ })
+
+ var currentState = m.prop()
+ function callNextStep(state) {
+ runner.step(state).then(currentState).then(function (newState) {
+ if (newState) callNextStep(newState)
+ })
+ }
+
+ // Don't call step while step is already executing.
+ document.body.appendChild(createUIForSuites(Suites,
+ function () {
+ runner.step(currentState()).then(currentState)
+ },
+ function () {
+ document.getElementById("analysis").style.display = "none"
+ localStorage.clear()
+ callNextStep(currentState())
+ }))
+ })
+})(this)
diff --git a/bench/resources/tests.js b/bench/resources/tests.js
new file mode 100644
index 00000000..872fcfa4
--- /dev/null
+++ b/bench/resources/tests.js
@@ -0,0 +1,51 @@
+(function (global) {
+ "use strict"
+
+ var BenchmarkTestStep = global.BenchmarkTestStep
+ var Suites = global.Suites = []
+
+ Suites.push(function (numberOfItemsToAdd) {
+ return {
+ name: "Mithril (TodoMVC 1.3)",
+ url: "app/index.html",
+ version: "next (dev version)",
+ prepare: function (runner) {
+ return runner.waitForElement("#new-todo")
+ .then(function (element) {
+ element.focus()
+ return element
+ })
+ },
+ tests: [
+ new BenchmarkTestStep("Adding" + numberOfItemsToAdd + "Items",
+ function (newTodo) {
+ for (var i = 0; i < numberOfItemsToAdd; i++) {
+ var inputEvent = document.createEvent("Event")
+ inputEvent.initEvent("input", true, true)
+ newTodo.value = "Mithril ------- Something to do " + i
+ newTodo.dispatchEvent(inputEvent)
+
+ var keydownEvent = document.createEvent("Event")
+ keydownEvent.initEvent("keyup", true, true)
+ keydownEvent.keyCode = 13 // VK_ENTER
+ newTodo.dispatchEvent(keydownEvent)
+ }
+ }),
+ new BenchmarkTestStep("CompletingAllItems",
+ function (newTodo, contentWindow, document) {
+ var checkboxes = document.getElementsByClassName("toggle")
+ for (var i = 0; i < checkboxes.length; i++) {
+ checkboxes[i].click()
+ }
+ }),
+ new BenchmarkTestStep("DeletingAllItems",
+ function (newTodo, contentWindow, document) {
+ var buttons = document.getElementsByClassName("destroy")
+ for (var i = buttons.length - 1; i > -1; i--) {
+ buttons[i].click()
+ }
+ })
+ ]
+ }
+ })
+})(this)
diff --git a/mithril.js b/mithril.js
index 2c9de619..bb4e567d 100644
--- a/mithril.js
+++ b/mithril.js
@@ -1,4 +1,4 @@
-void (function (global, factory) { // eslint-disable-line
+;(function (global, factory) { // eslint-disable-line
"use strict"
/* eslint-disable no-undef */
var m = factory(typeof window !== "undefined" ? window : {})
@@ -37,22 +37,20 @@ void (function (global, factory) { // eslint-disable-line
function noop() {}
- function forEach(list, f) {
- for (var i = 0; i < list.length && !f(list[i], i++);) {
- // empty
+ function forEach(list, f, inst) {
+ for (var i = 0; i < list.length; i++) {
+ f.call(inst, list[i], i)
}
}
- function forOwn(obj, f) {
+ function forOwn(obj, f, inst) {
for (var prop in obj) {
if (hasOwn.call(obj, prop)) {
- if (f(obj[prop], prop)) break
+ if (f.call(inst, obj[prop], prop)) break
}
}
}
- var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g
- var attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/
var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ // eslint-disable-line max-len
// caching commonly used variables
@@ -87,23 +85,23 @@ void (function (global, factory) { // eslint-disable-line
*/
function checkForAttrs(pairs) {
- return pairs != null &&
- isObject(pairs) &&
+ return pairs != null && isObject(pairs) &&
!("tag" in pairs || "view" in pairs || "subtree" in pairs)
}
function parseSelector(tag, cell) {
var classes = []
+ var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g
var match
while ((match = parser.exec(tag)) != null) {
- if (match[1] === "" && match[2]) {
+ if (match[1] === "" && match[2] != null) {
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 = attrParser.exec(match[3])
+ var pair = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3])
cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true)
}
}
@@ -111,34 +109,64 @@ void (function (global, factory) { // eslint-disable-line
return classes
}
- function getChildrenFromList(hasAttrs, args) {
- var children = hasAttrs ? args.slice(1) : args
- if (children.length === 1 && isArray(children[0])) {
- return children[0]
- } else {
- return children
- }
- }
-
- function assignAttrs(cell, attrs, classAttr, classes) {
- forOwn(attrs, function (value, attr) {
- if (attr === classAttr &&
- attrs[attr] != null &&
- attrs[attr] !== "") {
- classes.push(attrs[attr])
-
- // create key in correct iteration order
- cell.attrs[attr] = ""
- } else {
- cell.attrs[attr] = attrs[attr]
+ function assignAttrs(target, attrs, classAttr, classes) {
+ var hasClass = false
+ if (hasOwn.call(attrs, classAttr)) {
+ var value = attrs[classAttr]
+ if (value != null && value !== "") {
+ hasClass = true
+ classes.push(value)
}
+ }
+
+ forOwn(attrs, function (value, attr) {
+ target[attr] = attr === classAttr && hasClass ? "" : value
})
if (classes.length) {
- cell.attrs[classAttr] = classes.join(" ")
+ target[classAttr] = classes.join(" ")
}
}
+ function parameterize(component) {
+ var args = []
+ for (var i = 1; i < arguments.length; i++) {
+ args.push(arguments[i])
+ }
+
+ var originalCtrl = component.controller || noop
+
+ function Ctrl() {
+ return originalCtrl.apply(this, args) || this
+ }
+
+ if (originalCtrl !== noop) {
+ Ctrl.prototype = originalCtrl.prototype
+ }
+
+ var originalView = component.view || noop
+
+ function view(ctrl) {
+ var rest = [ctrl].concat(args)
+ for (var i = 1; i < arguments.length; i++) {
+ rest.push(arguments[i])
+ }
+
+ return originalView.apply(component, rest)
+ }
+
+ view.$original = originalView
+ var output = {controller: Ctrl, view: view}
+
+ if (args[0] && args[0].key != null) {
+ output.attrs = {key: args[0].key}
+ }
+
+ return output
+ }
+
+ m.component = parameterize
+
/**
* @param {Tag} The DOM node tag
* @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
@@ -146,34 +174,60 @@ void (function (global, factory) { // eslint-disable-line
* or splat (optional)
*/
function m(tag, pairs) {
- for (var args = [], i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i]
- }
-
- if (isObject(tag)) return parameterize(tag, args)
- var hasAttrs = checkForAttrs(pairs)
- var attrs = hasAttrs ? pairs : {}
- var classAttr = "class" in attrs ? "class" : "className"
- var cell = {tag: "div", attrs: {}}
+ // The arguments are passed directly like this to delay array
+ // allocation.
+ if (isObject(tag)) return parameterize.apply(null, arguments)
if (!isString(tag)) {
- throw new Error("selector in m(selector, attrs, children) should " +
- "be a string")
+ throw new TypeError("selector in m(selector, attrs, children) " +
+ "should be a string")
}
- var classes = parseSelector(tag, cell)
- cell.children = getChildrenFromList(hasAttrs, args)
+ // Degenerate case frequently trips people up. Check for it here so that
+ // people know it doesn't work.
+ if (!tag) {
+ throw new TypeError("selector cannot be an empty string")
+ }
- assignAttrs(cell, attrs, classAttr, classes)
+ var hasAttrs = checkForAttrs(pairs)
+
+ var args = []
+ for (var i = hasAttrs ? 2 : 1; i < arguments.length; i++) {
+ args.push(arguments[i])
+ }
+
+ var children
+
+ if (args.length === 1 && isArray(args[0])) {
+ children = args[0]
+ } else {
+ children = args
+ }
+
+ var cell = {
+ tag: "div",
+ attrs: {},
+ children: children
+ }
+
+ assignAttrs(
+ cell.attrs,
+ hasAttrs ? pairs : {},
+ hasAttrs && "class" in pairs ? "class" : "className",
+ parseSelector(tag, cell)
+ )
return cell
}
- function forKeys(list, f) {
- forEach(list, function (attrs, i) {
+ function forKeys(list, f, inst) {
+ for (var i = 0; i < list.length; i++) {
+ var attrs = list[i]
attrs = attrs && attrs.attrs
- return attrs && attrs.key != null && f(attrs, i)
- })
+ if (attrs && attrs.key != null && f.call(inst, attrs, i)) {
+ break
+ }
+ }
}
// This function was causing deopts in Chrome.
@@ -191,17 +245,6 @@ void (function (global, factory) { // eslint-disable-line
return ""
}
- // This function was causing deopts in Chrome.
- function injectTextNode(parent, first, index, data) {
- try {
- insertNode(parent, 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++) {
@@ -220,111 +263,129 @@ void (function (global, factory) { // eslint-disable-line
parent.insertBefore(node, parent.childNodes[index] || null)
}
+ // the below recursively manages creation/diffing/removal of DOM elements
+ // based on comparison between `data` and `cached`
+ //
+ // the diff algorithm can be summarized as this:
+ // 1) compare `data` and `cached`
+ // 2) if they are different, copy `data` to `cached` and update the DOM
+ // based on what the difference is
+ // 3) recursively apply this algorithm for every array and for the
+ // children of every virtual element
+ //
+ // the `cached` data structure is essentially the same as the previous
+ // redraw's `data` data structure, with a few additions:
+ // - `cached` always has a property called `nodes`, which is a list of
+ // DOM elements that correspond to the data represented by the
+ // respective virtual element
+ // - in order to support attaching `nodes` as a property of `cached`,
+ // `cached` is *always* a non-primitive object, i.e. if the data was
+ // a string, then cached is a String instance. If data was `null` or
+ // `undefined`, cached is `new String("")`
+ // - `cached also has a `configContext` property, which is the state
+ // storage object exposed by config(element, isInitialized, context)
+ // - when `cached` is an Object, it represents a virtual element; when
+ // it's an Array, it represents a list of elements; when it's a
+ // String, Number or Boolean, it represents a text node
+ //
+ // `parentElement` is a DOM element used for W3C DOM API calls
+ // `parentTag` is only used for handling a corner case for textarea
+ // values
+ // `parentCache` is used to remove nodes in some multi-node cases
+ // `parentIndex` and `index` are used to figure out the offset of nodes.
+ // They're artifacts from before arrays started being flattened and are
+ // likely refactorable
+ // `data` and `cached` are, respectively, the new and old nodes being
+ // diffed
+ // `shouldReattach` is a flag indicating whether a parent node was
+ // recreated (if so, and if this node is reused, then this node must
+ // reattach itself to the new parent)
+ // `editable` is a flag that indicates whether an ancestor is
+ // contenteditable
+ // `namespace` indicates the closest HTML namespace as it cascades down
+ // from an ancestor
+ // `configs` is a list of config functions to run after the topmost
+ // `build` call finishes running
+ //
+ // there's logic that relies on the assumption that null and undefined
+ // data are equivalent to empty strings
+ // - this prevents lifecycle surprises from procedural helpers that mix
+ // implicit and explicit return statements (e.g.
+ // function foo() {if (cond) return m("div")}
+ // - it simplifies diffing code
+
+ function Builder(
+ parentElement,
+ parentTag,
+ parentCache,
+ parentIndex,
+ data,
+ cached,
+ shouldReattach,
+ index,
+ editable,
+ namespace,
+ configs
+ ) {
+ this.parentElement = parentElement
+ this.parentTag = parentTag
+ this.parentCache = parentCache
+ this.parentIndex = parentIndex
+ this.data = data
+ this.cached = cached
+ this.shouldReattach = shouldReattach
+ this.index = index
+ this.editable = editable
+ this.namespace = namespace
+ this.configs = configs
+ }
+
+ Builder.prototype.build = function () {
+ this.data = dataToString(this.data)
+ if (this.data.subtree === "retain") return this.cached
+ this.makeCache()
+
+ if (isArray(this.data)) {
+ return this.buildArray()
+ } else if (this.data != null && isObject(this.data)) {
+ return this.buildObject()
+ } else if (isFunction(this.data)) {
+ return this.cached
+ } else {
+ return this.handleTextNode()
+ }
+ }
+
+ Builder.prototype.makeCache = function () {
+ if (this.cached != null) {
+ if (type.call(this.cached) === type.call(this.data)) {
+ return
+ }
+
+ if (this.parentCache && this.parentCache.nodes) {
+ var offset = this.index - this.parentIndex
+ var end = offset +
+ (isArray(this.data) ? this.data : this.cached.nodes).length
+
+ clear(
+ this.parentCache.nodes.slice(offset, end),
+ this.parentCache.slice(offset, end))
+ } else if (this.cached.nodes) {
+ clear(this.cached.nodes, this.cached)
+ }
+ }
+
+ this.cached = new this.data.constructor()
+ // if constructor creates a virtual dom element, use a blank object as
+ // the base cached node instead of copying the virtual el (#277)
+ if (this.cached.tag) this.cached = {}
+ this.cached.nodes = []
+ }
+
var DELETION = 1
var INSERTION = 2
var MOVE = 3
- function handleKeysDiffer(data, existing, cached, parent) {
- forKeys(data, function (key, i) {
- key = key.key
- if (existing[key]) {
- existing[key] = {
- action: MOVE,
- index: i,
- from: existing[key].index,
- element: cached.nodes[existing[key].index] ||
- $document.createElement("div")
- }
- } else {
- existing[key] = {action: INSERTION, index: i}
- }
- })
-
- var actions = []
-
- forOwn(existing, function (value) {
- actions.push(value)
- })
-
- var changes = actions.sort(sortChanges)
- var newCached = new Array(cached.length)
- newCached.nodes = cached.nodes.slice()
-
- forEach(changes, function (change) {
- var index = change.index
-
- switch (change.action) {
- case DELETION:
- clear(cached[index].nodes, cached[index])
- newCached.splice(index, 1)
- break
-
- case INSERTION:
- var dummy = $document.createElement("div")
- dummy.key = data[index].attrs.key
- insertNode(parent, dummy, index)
- newCached.splice(index, 0, {
- attrs: {key: data[index].attrs.key},
- nodes: [dummy]
- })
- newCached.nodes[index] = dummy
- break
-
- case MOVE:
- var changeElement = change.element
- var maybeChanged = parent.childNodes[index]
- if (maybeChanged !== changeElement && changeElement !== null) {
- parent.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
- }
- }
-
- // diffs the array itself
- function diffArray(data, cached, nodes) {
- // 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 () {
@@ -338,6 +399,317 @@ void (function (global, factory) { // eslint-disable-line
})
}
+ Builder.prototype.buildArrayChild = function (child, cached, count) {
+ return new Builder(
+ this.parentElement,
+ this.parentTag,
+ this.cached,
+ this.index,
+ child,
+ cached,
+ this.shouldReattach,
+ this.index + count || count,
+ this.editable,
+ this.namespace,
+ this.configs
+ ).build()
+ }
+
+ Builder.prototype.buildArray = function () {
+ this.data = flatten(this.data)
+ var nodes = []
+ var intact = this.cached.length === this.data.length
+ var subArrayCount = 0
+
+ // keys algorithm:
+ // sort elements without recreating them if keys are present
+ //
+ // 1) create a map of all existing keys, and mark all for deletion
+ // 2) add new keys to map and mark them for addition
+ // 3) if key exists in new list, change action from deletion to a move
+ // 4) for each key, handle its corresponding action as marked in
+ // previous steps
+ var existing = {}
+ var shouldMaintainIdentities = false
+ forKeys(this.cached, function (attrs, i) {
+ shouldMaintainIdentities = true
+ existing[attrs.key] = {
+ action: DELETION,
+ index: i
+ }
+ })
+
+ buildArrayKeys(this.data)
+ if (shouldMaintainIdentities) {
+ this.diffKeys(existing)
+ }
+ // end key algorithm
+
+ // don't change: faster than forEach
+ var cacheCount = 0
+ for (var i = 0, len = this.data.length; i < len; i++) {
+ // diff each item in the array
+ var item = this.buildArrayChild(
+ this.data[i],
+ this.cached[cacheCount],
+ subArrayCount
+ )
+
+ if (item !== undefined) {
+ intact = intact && item.nodes.intact
+ subArrayCount += getSubArrayCount(item)
+ this.cached[cacheCount++] = item
+ }
+ }
+
+ if (!intact) this.diffArray(nodes)
+
+ return this.cached
+ }
+
+ Builder.prototype.diffKeys = function (existing) {
+ var keysDiffer = this.data.length !== this.cached.length
+
+ if (!keysDiffer) {
+ forKeys(this.data, function (attrs, i) {
+ var cachedCell = this[i] // eslint-disable-line no-invalid-this
+ return keysDiffer = cachedCell &&
+ cachedCell.attrs &&
+ cachedCell.attrs.key !== attrs.key
+ }, this.cached)
+ }
+
+ if (keysDiffer) {
+ this.handleKeysDiffer(existing)
+ }
+ }
+
+ // Simple `this` helper
+ function thisPush(value) {
+ this.push(value) // eslint-disable-line no-invalid-this
+ }
+
+ Builder.prototype.handleKeysDiffer = function (existing) {
+ forKeys(this.data, function (key, i) {
+ key = key.key
+ if (existing[key]) {
+ existing[key] = {
+ action: MOVE,
+ index: i,
+ from: existing[key].index,
+ element: this[existing[key].index] || // eslint-disable-line
+ $document.createElement("div")
+ }
+ } else {
+ existing[key] = {
+ action: INSERTION,
+ index: i
+ }
+ }
+ }, this.cached.nodes)
+
+ var actions = []
+ forOwn(existing, thisPush, actions)
+
+ var changes = actions.sort(sortChanges)
+ var newCached = new Array(this.cached.length)
+ newCached.nodes = this.cached.nodes.slice()
+
+ forEach(changes, function (change) {
+ /* eslint-disable no-invalid-this */
+ var index = change.index
+
+ switch (change.action) {
+ case DELETION:
+ clear(this.cached[index].nodes, this.cached[index])
+ newCached.splice(index, 1)
+ break
+
+ case INSERTION:
+ var dummy = $document.createElement("div")
+ dummy.key = this.data[index].attrs.key
+ insertNode(this.parentElement, dummy, index)
+ newCached.splice(index, 0, {
+ attrs: {key: this.data[index].attrs.key},
+ nodes: [dummy]
+ })
+ newCached.nodes[index] = dummy
+ break
+
+ case MOVE:
+ var changeElement = change.element
+ var maybeChanged = this.parentElement.childNodes[index]
+ if (maybeChanged !== changeElement && changeElement !== null) {
+ this.parentElement.insertBefore(
+ changeElement,
+ maybeChanged || null
+ )
+ }
+ newCached[index] = this.cached[change.from]
+ newCached.nodes[index] = changeElement
+ }
+ /* eslint-enable no-invalid-this */
+ }, this)
+
+ this.cached = newCached
+ }
+
+ // diffs the array itself
+ Builder.prototype.diffArray = function (nodes) {
+ // update the list of DOM nodes by collecting the nodes from each item
+ for (var i = 0; i < this.data.length; i++) {
+ var cached = this.cached[i]
+ if (cached != null) {
+ nodes.push.apply(nodes, cached.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(this.cached.nodes, function (node, i) {
+ /* eslint-disable no-invalid-this */
+ if (node.parentNode != null && nodes.indexOf(node) < 0) {
+ clear([node], [this[i]])
+ }
+ /* eslint-enable no-invalid-this */
+ }, this.cached)
+
+ if (this.data.length < this.cached.length) {
+ this.cached.length = this.data.length
+ }
+
+ this.cached.nodes = nodes
+ }
+
+ Builder.prototype.initAttrs = function () {
+ var dataAttrs = this.data.attrs = this.data.attrs || {}
+ this.cached.attrs = this.cached.attrs || {}
+
+ var dataAttrKeys = Object.keys(this.data.attrs)
+ this.maybeRecreateObject(dataAttrKeys)
+
+ return dataAttrKeys.length > +("key" in dataAttrs)
+ }
+
+ Builder.prototype.buildObject = function () {
+ var views = []
+ var controllers = []
+
+ this.markViews(views, controllers)
+
+ if (!this.data.tag && controllers.length) {
+ throw new Error("Component template must return a virtual " +
+ "element, not an array, string, etc.")
+ }
+
+ var hasKeys = this.initAttrs()
+
+ if (isString(this.data.tag)) {
+ return new ObjectBuilder(
+ this,
+ hasKeys,
+ views,
+ controllers
+ ).build()
+ }
+ }
+
+ Builder.prototype.markViews = function (views, controllers) {
+ var cached = this.cached && this.cached.controllers
+ while (this.data.view != null) {
+ this.checkView(cached, controllers, views)
+ }
+ }
+
+ var forcing = false
+ var pendingRequests = 0
+
+ Builder.prototype.checkView = function (cached, controllers, views) {
+ var view = this.data.view.$original || this.data.view
+ var controller = getController(
+ this.cached.views,
+ view,
+ cached,
+ this.data.controller
+ )
+
+ // Faster to coerce to number and check for NaN
+ var key = +(this.data && this.data.attrs && this.data.attrs.key)
+
+ if (pendingRequests === 0 || forcing ||
+ cached && cached.indexOf(controller) > -1) {
+ this.data = this.data.view(controller)
+ } else {
+ this.data = {tag: "placeholder"}
+ }
+
+ if (this.data.subtree === "retain") return this.cached
+ if (key === key) { // eslint-disable-line no-self-compare
+ (this.data.attrs = this.data.attrs || {}).key = key
+ }
+ updateLists(views, controllers, view, controller)
+ }
+
+ var unloaders = []
+
+ function updateLists(views, controllers, view, controller) {
+ views.push(view)
+ var idx = controllers.push(controller) - 1
+ unloaders[idx] = {
+ views: views,
+ view: view,
+ controller: controller,
+ controllers: controllers,
+ handler: function (ev) {
+ var i = this.controllers.indexOf(this.controller)
+ this.controllers.splice(i, 1)
+ i = this.views.indexOf(this.view)
+ this.views.splice(i, 1)
+ var unload = this.controller && this.controller.onunload
+ if (type.call(unload) === "[object Function]") {
+ this.controller.onunload(ev)
+ }
+ }
+ }
+ }
+
+ function getController(views, view, cached, controller) {
+ var index = m.redraw.strategy() === "diff" && views ?
+ views.indexOf(view) :
+ -1
+
+ if (index > -1) {
+ return cached[index]
+ } else if (typeof controller === "function") {
+ return new controller()
+ } else {
+ return {}
+ }
+ }
+
+ function unloadSingleController(controller) {
+ if (controller.unload) {
+ controller.onunload({preventDefault: noop})
+ }
+ }
+
+ Builder.prototype.maybeRecreateObject = function (dataAttrKeys) {
+ // if an element is different enough from the one in cache, recreate it
+ if (this.elemIsDifferentEnough(dataAttrKeys)) {
+ if (this.cached.nodes.length) clear(this.cached.nodes)
+ if (this.cached.configContext &&
+ isFunction(this.cached.configContext.onunload)) {
+ this.cached.configContext.onunload()
+ }
+
+ if (this.cached.controllers) {
+ forEach(this.cached.controllers, unloadSingleController)
+ }
+ }
+ }
+
// shallow array compare, sorts
function arraySortCompare(a, b) {
a.sort()
@@ -350,7 +722,9 @@ void (function (global, factory) { // eslint-disable-line
return true
}
- function elemIsDifferentEnough(data, cached, dataAttrKeys) {
+ Builder.prototype.elemIsDifferentEnough = function (dataAttrKeys) {
+ var data = this.data
+ var cached = this.cached
if (data.tag !== cached.tag) return true
if (!arraySortCompare(dataAttrKeys, Object.keys(cached.attrs))) {
return true
@@ -360,70 +734,174 @@ void (function (global, factory) { // eslint-disable-line
if (data.attrs.key !== cached.attrs.key) return true
if (m.redraw.strategy() === "all") {
- return !(cached.configContext &&
- cached.configContext.retain === true)
+ return !cached.configContext || cached.configContext.retain !== true
} else if (m.redraw.strategy() === "diff") {
- return cached.configContext &&
- cached.configContext.retain === false
+ return cached.configContext && cached.configContext.retain === false
+ } else {
+ return false
}
}
- function maybeRecreateObject(data, cached, dataAttrKeys) {
- // if an element is different enough from the one in cache, recreate it
- if (elemIsDifferentEnough(data, cached, dataAttrKeys)) {
- if (cached.nodes.length) clear(cached.nodes)
- if (cached.configContext &&
- isFunction(cached.configContext.onunload)) {
- cached.configContext.onunload()
- }
+ function getObjectNamespace(builder) {
+ var data = builder.data
- if (cached.controllers) {
- forEach(cached.controllers, function (controller) {
- if (controller.unload) {
- controller.onunload({preventDefault: noop})
- }
- })
- }
- }
- }
-
- function getObjectNamespace(data, namespace) {
return data.attrs.xmlns ? data.attrs.xmlns :
data.tag === "svg" ? "http://www.w3.org/2000/svg" :
data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" :
- namespace
+ builder.namespace
}
- var pendingRequests = 0
- m.startComputation = function () { pendingRequests++ }
- m.endComputation = function () {
- if (pendingRequests > 1) {
- pendingRequests--
+ function ObjectBuilder(builder, hasKeys, views, controllers) {
+ this.builder = builder
+ this.hasKeys = hasKeys
+ this.views = views
+ this.controllers = controllers
+ this.namespace = getObjectNamespace(builder)
+ }
+
+ ObjectBuilder.prototype.buildNewNode = function () {
+ var node = this.createNode()
+ this.builder.cached = this.reconstruct(
+ node,
+ this.createAttrs(node),
+ this.buildChildren(node)
+ )
+ return node
+ }
+
+ ObjectBuilder.prototype.build = function () {
+ var builder = this.builder
+ var isNew = builder.cached.nodes.length === 0
+ var node = isNew ? this.buildNewNode() : this.buildUpdatedNode()
+ if (isNew || builder.shouldReattach && node != null) {
+ insertNode(builder.parentElement, node, builder.index)
+ }
+ builder.scheduleConfigs(node, isNew)
+ return builder.cached
+ }
+
+ ObjectBuilder.prototype.createNode = function () {
+ var data = this.builder.data
+ if (this.namespace === undefined) {
+ if (data.attrs.is) {
+ return $document.createElement(data.tag, data.attrs.is)
+ } else {
+ return $document.createElement(data.tag)
+ }
+ } else if (data.attrs.is) {
+ return $document.createElementNS(this.namespace, data.tag,
+ data.attrs.is)
} else {
- pendingRequests = 0
- m.redraw()
+ return $document.createElementNS(this.namespace, data.tag)
}
}
- 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
- }
- })
+ ObjectBuilder.prototype.createAttrs = function (node) {
+ var data = this.builder.data
+ if (this.hasKeys) {
+ return setAttributes(node, data.tag, data.attrs, {}, this.namespace)
+ } else {
+ return data.attrs
}
}
- function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) {
+ ObjectBuilder.prototype.makeChild = function (node, shouldReattach) {
+ var builder = this.builder
+ return new Builder(
+ node,
+ builder.data.tag,
+ undefined,
+ undefined,
+ builder.data.children,
+ builder.cached.children,
+ shouldReattach,
+ 0,
+ builder.data.attrs.contenteditable ? node : builder.editable,
+ this.namespace,
+ builder.configs
+ ).build()
+ }
+
+ ObjectBuilder.prototype.buildChildren = function (node) {
+ var data = this.builder.data
+ if (data.children != null && data.children.length !== 0) {
+ return this.makeChild(node, true)
+ } else {
+ return data.children
+ }
+ }
+
+ ObjectBuilder.prototype.reconstruct = function (node, attrs, children) {
+ var data = this.builder.data
+ var cached = {
+ tag: data.tag,
+ attrs: attrs,
+ children: children,
+ nodes: [node]
+ }
+
+ this.unloadCachedControllers(cached)
+
+ if (cached.children && !cached.children.nodes) {
+ cached.children.nodes = []
+ }
+
+ // edge case: setting value on doesn't work before children
+ // exist, so set it again after children have been created
+ if (data.tag === "select" && "value" in data.attrs) {
+ setAttributes(node, data.tag, {value: data.attrs.value}, {},
+ this.namespace)
+ }
+ return cached
+ }
+
+ function unloadSingleCachedController(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
+ }
+ }
+
+ ObjectBuilder.prototype.unloadCachedControllers = function (cached) {
+ if (this.controllers.length) {
+ cached.views = this.views
+ cached.controllers = this.controllers
+ forEach(this.controllers, unloadSingleCachedController)
+ }
+ }
+
+ ObjectBuilder.prototype.buildUpdatedNode = function () {
+ var builder = this.builder
+ var node = builder.cached.nodes[0]
+ if (this.hasKeys) {
+ setAttributes(
+ node,
+ builder.data.tag,
+ builder.data.attrs,
+ builder.cached.attrs,
+ this.namespace
+ )
+ }
+
+ builder.cached.children = this.makeChild(node, false)
+ builder.cached.nodes.intact = true
+
+ if (this.controllers.length) {
+ builder.cached.views = this.views
+ builder.cached.controllers = this.controllers
+ }
+
+ return node
+ }
+
+ Builder.prototype.scheduleConfigs = function (node, isNew) {
+ var data = this.data
+ var cached = this.cached
// schedule configs to be called. They are called after `build` finishes
// running
var config = data.attrs.config
@@ -431,60 +909,43 @@ void (function (global, factory) { // eslint-disable-line
var context = cached.configContext = cached.configContext || {}
// bind
- configs.push(function () {
+ this.configs.push(function () {
return 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)
+ Builder.prototype.handleTextNode = function () {
+ if (this.cached.nodes.length === 0) {
+ return this.handleNonexistentNodes()
+ } else if (this.cached.valueOf() !== this.data.valueOf() ||
+ this.shouldReattach) {
+ return this.reattachNodes()
+ } else {
+ this.cached.nodes.intact = true
+ return this.cached
}
-
- 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, parent, index) {
+ Builder.prototype.handleNonexistentNodes = function () {
var nodes
- if (data.$trusted) {
- nodes = injectHTML(parent, index, data)
+ if (this.data.$trusted) {
+ nodes = injectHTML(this.parentElement, this.index, this.data)
} else {
- nodes = [$document.createTextNode(data)]
- if (!voidElements.test(parent.nodeName)) {
- insertNode(parent, nodes[0], index)
+ nodes = [$document.createTextNode(this.data)]
+ if (!voidElements.test(this.parentElement.nodeName)) {
+ insertNode(this.parentElement, nodes[0], this.index)
}
}
var cached
- if (typeof data === "string" ||
- typeof data === "number" ||
- typeof data === "boolean") {
- cached = new data.constructor(data)
+ if (typeof this.data === "string" ||
+ typeof this.data === "number" ||
+ typeof this.data === "boolean") {
+ cached = new this.data.constructor(this.data)
} else {
- cached = data
+ cached = this.data
}
cached.nodes = nodes
@@ -492,59 +953,55 @@ void (function (global, factory) { // eslint-disable-line
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") {
+ Builder.prototype.reattachNodes = function () {
+ var nodes = this.cached.nodes
+ if (!this.editable || this.editable !== $document.activeElement) {
+ if (this.data.$trusted) {
+ clear(nodes, this.cached)
+ nodes = injectHTML(this.parentElement, this.index, this.data)
+ } else if (this.parentTag === "textarea") {
//