diff --git a/.eslintignore b/.eslintignore index 1b9fe34c..61d2a0b7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,11 +1,7 @@ -coverage .vscode -examples -docs -node_modules -tests -test-utils -ospec -mithril.js -mithril.min.js -archive +/coverage +/docs/lib +/examples +/mithril.js +/mithril.min.js +/node_modules diff --git a/.eslintrc.js b/.eslintrc.js index 25278201..c14d0119 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,14 @@ module.exports = { "id-blacklist": "error", "id-length": "off", "id-match": "error", - "indent": "off", + "indent": [ + "warn", + "tab", + { + "outerIIFEBody": 0, + "SwitchCase": 1 + } + ], "init-declarations": "off", "jsx-quotes": "error", "key-spacing": "off", @@ -188,7 +195,7 @@ module.exports = { "quotes": [ "error", "double", - "avoid-escape" + {"avoidEscape": true} ], "radix": [ "error", @@ -209,7 +216,7 @@ module.exports = { "space-infix-ops": "off", "space-unary-ops": "error", "spaced-comment": "off", - "strict": "off", + "strict": ["error", "global"], "template-curly-spacing": "error", "valid-jsdoc": "off", "vars-on-top": "off", @@ -217,5 +224,6 @@ module.exports = { "wrap-regex": "error", "yield-star-spacing": "error", "yoda": "off" - } -}; \ No newline at end of file + }, + "root": true +}; diff --git a/.gitattributes b/.gitattributes index 21256661..574ffd50 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ -* text=auto \ No newline at end of file +* text=auto +/mithril.js binary +/mithril.min.js binary +/stream/stream.js binary diff --git a/.gitignore b/.gitignore index 8af32a76..285af458 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ jsconfig.json npm-debug.log .vscode .DS_Store +.eslintcache diff --git a/README.md b/README.md index cdcd540c..08636be2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Mithril is used by companies like Vimeo and Nike, and open source platforms like If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](http://mithril.js.org/framework-comparison.html) page. +Mithril supports browsers all the way back to IE9, no polyfills required. + --- ### Getting started @@ -54,7 +56,7 @@ Let's create an HTML file to follow along: ```markup - + + diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index 210a0627..db2bee04 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var m = require("../../render/hyperscript") @@ -22,217 +23,233 @@ o.spec("mount", function() { render = coreRenderer($window).render }) - o("throws on invalid `root` DOM node", function() { + o("throws on invalid component", function() { var threw = false try { - mount(null, {view: function() {}}) + mount(root, {}) } catch (e) { threw = true } o(threw).equals(true) }) - o("renders into `root`", function() { - mount(root, { - view : function() { - return m("div") - } - }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - o(root.firstChild.nodeName).equals("DIV") - }) + o("throws on invalid `root` DOM node", function() { + var threw = false + try { + mount(null, createComponent({view: function() {}})) + } catch (e) { + threw = true + } + o(threw).equals(true) + }) - o("mounting null unmounts", function() { - mount(root, { - view : function() { - return m("div") - } - }) - - mount(root, null) - - o(root.childNodes.length).equals(0) - }) - - o("redraws on events", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var onclick = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, { - view : function() { - return m("div", { - oninit : oninit, - onupdate : onupdate, - onclick : onclick, - }) - } - }) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - o(onclick.callCount).equals(1) - o(onclick.this).equals(root.firstChild) - o(onclick.args[0].type).equals("click") - o(onclick.args[0].target).equals(root.firstChild) - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - done() - }, FRAME_BUDGET) - }) - - o("redraws several mount points on events", function(done, timeout) { - timeout(60) - - var onupdate0 = o.spy() - var oninit0 = o.spy() - var onclick0 = o.spy() - var onupdate1 = o.spy() - var oninit1 = o.spy() - var onclick1 = o.spy() - - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - render(root, [ - m("#child0"), - m("#child1") - ]) - - mount(root.childNodes[0], { - view : function() { - return m("div", { - oninit : oninit0, - onupdate : onupdate0, - onclick : onclick0, - }) - } - }) - - o(oninit0.callCount).equals(1) - o(onupdate0.callCount).equals(0) - - mount(root.childNodes[1], { - view : function() { - return m("div", { - oninit : oninit1, - onupdate : onupdate1, - onclick : onclick1, - }) - } - }) - - o(oninit1.callCount).equals(1) - o(onupdate1.callCount).equals(0) - - root.childNodes[0].firstChild.dispatchEvent(e) - o(onclick0.callCount).equals(1) - o(onclick0.this).equals(root.childNodes[0].firstChild) - - setTimeout(function() { - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) - - root.childNodes[1].firstChild.dispatchEvent(e) - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].firstChild) - - setTimeout(function() { - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - - }) - - o("event handlers can skip redraw", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - var e = $window.document.createEvent("MouseEvents") - - e.initEvent("click", true, true) - - mount(root, { - view: function() { - return m("div", { - oninit: oninit, - onupdate: onupdate, - onclick: function(e) { - e.redraw = false + o("renders into `root`", function() { + mount(root, createComponent({ + view : function() { + return m("div") } - }) - } + })) + + o(root.firstChild.nodeName).equals("DIV") + }) + + o("mounting null unmounts", function() { + mount(root, createComponent({ + view : function() { + return m("div") + } + })) + + mount(root, null) + + o(root.childNodes.length).equals(0) + }) + + o("redraws on events", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var onclick = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + mount(root, createComponent({ + view : function() { + return m("div", { + oninit : oninit, + onupdate : onupdate, + onclick : onclick, + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + o(onclick.callCount).equals(1) + o(onclick.this).equals(root.firstChild) + o(onclick.args[0].type).equals("click") + o(onclick.args[0].target).equals(root.firstChild) + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + + o("redraws several mount points on events", function(done, timeout) { + timeout(60) + + var onupdate0 = o.spy() + var oninit0 = o.spy() + var onclick0 = o.spy() + var onupdate1 = o.spy() + var oninit1 = o.spy() + var onclick1 = o.spy() + + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + render(root, [ + m("#child0"), + m("#child1") + ]) + + mount(root.childNodes[0], createComponent({ + view : function() { + return m("div", { + oninit : oninit0, + onupdate : onupdate0, + onclick : onclick0, + }) + } + })) + + o(oninit0.callCount).equals(1) + o(onupdate0.callCount).equals(0) + + mount(root.childNodes[1], createComponent({ + view : function() { + return m("div", { + oninit : oninit1, + onupdate : onupdate1, + onclick : onclick1, + }) + } + })) + + o(oninit1.callCount).equals(1) + o(onupdate1.callCount).equals(0) + + root.childNodes[0].firstChild.dispatchEvent(e) + o(onclick0.callCount).equals(1) + o(onclick0.this).equals(root.childNodes[0].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) + + root.childNodes[1].firstChild.dispatchEvent(e) + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root.childNodes[1].firstChild) + + setTimeout(function() { + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + + }) + + o("event handlers can skip redraw", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + + mount(root, createComponent({ + view: function() { + return m("div", { + oninit: oninit, + onupdate: onupdate, + onclick: function(e) { + e.redraw = false + } + }) + } + })) + + root.firstChild.dispatchEvent(e) + + o(oninit.callCount).equals(1) + + // Wrapped to ensure no redraw fired + setTimeout(function() { + o(onupdate.callCount).equals(0) + + done() + }, FRAME_BUDGET) + }) + + o("redraws when the render function is run", function(done) { + var onupdate = o.spy() + var oninit = o.spy() + + mount(root, createComponent({ + view : function() { + return m("div", { + oninit: oninit, + onupdate: onupdate + }) + } + })) + + o(oninit.callCount).equals(1) + o(onupdate.callCount).equals(0) + + redrawService.redraw() + + // Wrapped to give time for the rate-limited redraw to fire + setTimeout(function() { + o(onupdate.callCount).equals(1) + + done() + }, FRAME_BUDGET) + }) + + o("throttles", function(done, timeout) { + timeout(200) + + var i = 0 + mount(root, createComponent({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) + }) }) - - root.firstChild.dispatchEvent(e) - - o(oninit.callCount).equals(1) - - // Wrapped to ensure no redraw fired - setTimeout(function() { - o(onupdate.callCount).equals(0) - - done() - }, FRAME_BUDGET) - }) - - o("redraws when the render function is run", function(done) { - var onupdate = o.spy() - var oninit = o.spy() - - mount(root, { - view : function() { - return m("div", { - oninit: oninit, - onupdate: onupdate - }) - } - }) - - o(oninit.callCount).equals(1) - o(onupdate.callCount).equals(0) - - redrawService.redraw() - - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) - - 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-router.js b/api/tests/test-router.js index 8fec9042..9406de70 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -6,7 +6,6 @@ var browserMock = require("../../test-utils/browserMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") -var coreRenderer = require("../../render/render") var apiRedraw = require("../../api/redraw") var apiRouter = require("../../api/router") var Promise = require("../../promise/promise") @@ -31,7 +30,7 @@ o.spec("route", function() { o("throws on invalid `root` DOM node", function() { var threw = false try { - route(null, '/', {'/':{view: function() {}}}) + route(null, "/", {"/":{view: function() {}}}) } catch (e) { threw = true } @@ -51,7 +50,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("routed mount points can redraw synchronously (#1275)", function() { + o("routed mount points can redraw synchronously (POJO component)", function() { var view = o.spy() $window.location.href = prefix + "/" @@ -65,6 +64,39 @@ o.spec("route", function() { }) + o("routed mount points can redraw synchronously (constructible component)", function() { + var view = o.spy() + + var Cmp = function(){} + Cmp.prototype.view = view + + $window.location.href = prefix + "/" + route(root, "/", {"/":Cmp}) + + o(view.callCount).equals(1) + + redrawService.redraw() + + o(view.callCount).equals(2) + + }) + + o("routed mount points can redraw synchronously (closure component)", function() { + var view = o.spy() + + function Cmp() {return {view: view}} + + $window.location.href = prefix + "/" + route(root, "/", {"/":Cmp}) + + o(view.callCount).equals(1) + + redrawService.redraw() + + o(view.callCount).equals(2) + + }) + o("default route doesn't break back button", function(done) { $window.location.href = "http://old.com" $window.location.href = "http://new.com" @@ -173,7 +205,6 @@ 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 e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) @@ -321,11 +352,6 @@ o.spec("route", function() { o("accepts RouteResolver with onmatch that returns Promise", function(done) { var matchCount = 0 var renderCount = 0 - var Component = { - view: function() { - return m("span") - } - } var resolver = { onmatch: function(args, requestedPath) { @@ -362,11 +388,6 @@ o.spec("route", function() { o("accepts RouteResolver with onmatch that returns Promise", function(done) { var matchCount = 0 var renderCount = 0 - var Component = { - view: function() { - return m("span") - } - } var resolver = { onmatch: function(args, requestedPath) { @@ -404,14 +425,9 @@ o.spec("route", function() { var matchCount = 0 var renderCount = 0 var spy = o.spy() - var Component = { - view: function() { - return m("span") - } - } var resolver = { - onmatch: function(args, requestedPath) { + onmatch: function() { matchCount++ return Promise.reject(new Error("error")) }, @@ -466,7 +482,7 @@ o.spec("route", function() { }) }) - o("changing `vnode.key` in `render` resets the component", function(done, timeout){ + o("changing `vnode.key` in `render` resets the component", function(done){ var oninit = o.spy() var Component = { oninit: oninit, @@ -512,25 +528,19 @@ o.spec("route", function() { }) o(root.firstChild.nodeName).equals("DIV") + o(renderCount).equals(1) }) o("RouteResolver `render` does not have component semantics", function(done) { - var renderCount = 0 - var A = { - view: function() { - return m("div") - } - } - $window.location.href = prefix + "/a" route(root, "/a", { "/a" : { - render: function(vnode) { + render: function() { return m("div") }, }, "/b" : { - render: function(vnode) { + render: function() { return m("div") }, }, @@ -599,7 +609,7 @@ o.spec("route", function() { onmatch: function() { matchCount++ }, - render: function(vnode) { + render: function() { renderCount++ return {tag: Component} }, @@ -693,7 +703,7 @@ o.spec("route", function() { render: render }, "/b" : { - render: function(vnode){ + render: function(){ redirected = true } } @@ -805,7 +815,7 @@ o.spec("route", function() { }) callAsync(function() { - route.set('/b') + route.set("/b") callAsync(function() { callAsync(function() { callAsync(function() { @@ -832,7 +842,7 @@ o.spec("route", function() { render: render }, "/b" : { - onmatch: function(vnode){ + onmatch: function(){ redirected = true return {view: function() {}} } @@ -862,7 +872,7 @@ o.spec("route", function() { render: render }, "/b" : { - render: function(vnode){ + render: function(){ redirected = true } } @@ -891,7 +901,7 @@ o.spec("route", function() { render: render }, "/b" : { - view: function(vnode){ + view: function(){ redirected = true } } @@ -999,7 +1009,7 @@ o.spec("route", function() { var render = o.spy(function() {return m("div")}) $window.location.href = prefix + "/" - route(root, '/', { + route(root, "/", { "/": { onmatch: onmatch, render: render @@ -1048,23 +1058,23 @@ o.spec("route", function() { o("routing with RouteResolver works more than once", function(done) { $window.location.href = prefix + "/a" - route(root, '/a', { - '/a': { + route(root, "/a", { + "/a": { render: function() { return m("a", "a") } }, - '/b': { + "/b": { render: function() { return m("b", "b") } } }) - route.set('/b') + route.set("/b") callAsync(function() { - route.set('/a') + route.set("/a") callAsync(function() { o(root.firstChild.nodeName).equals("A") @@ -1089,7 +1099,7 @@ o.spec("route", function() { }) }) }, - render: function(vnode) { + render: function() { rendered = true resolved = "a" } @@ -1147,7 +1157,7 @@ o.spec("route", function() { route.set("/b") }) }, - render: function(vnode) { + render: function() { rendered = true resolved = "a" } @@ -1177,7 +1187,7 @@ o.spec("route", function() { var i = 0 $window.location.href = prefix + "/" route(root, "/", { - "/": {view: function(v) {i++}} + "/": {view: function() {i++}} }) var before = i diff --git a/browser.js b/browser.js index fed323f1..0debb4ea 100644 --- a/browser.js +++ b/browser.js @@ -1,3 +1,5 @@ +"use strict" + var m = require("./index") if (typeof module !== "undefined") module["exports"] = m else window.m = m diff --git a/bundler/bin/bundle b/bundler/bin/bundle index e0c4279a..81c65c90 100644 --- a/bundler/bin/bundle +++ b/bundler/bin/bundle @@ -1,3 +1,4 @@ #!/usr/bin/env node +"use strict" require("../cli") diff --git a/bundler/bundle.js b/bundler/bundle.js index 34148951..54971214 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -114,7 +114,7 @@ function run(input, output) { .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks .replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version - code = "new function() {\n" + code + "\n}" + code = ";(function() {\n" + code + "\n}());" if (!isFile(output) || code !== read(output)) { //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {} diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 3055bf7c..15e7f932 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -1,3 +1,5 @@ +"use strict" + var o = require("../../ospec/ospec") var bundle = require("../bundle") @@ -5,10 +7,10 @@ var fs = require("fs") var ns = "bundler/tests/" function read(filepath) { - try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {} + try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {/* ignore */} } function write(filepath, data) { - try {var exists = fs.statSync(ns + filepath).isFile()} catch (e) {} + try {var exists = fs.statSync(ns + filepath).isFile()} catch (e) {/* ignore */} if (exists) throw new Error("Don't call `write('" + filepath + "')`. Cannot overwrite file") fs.writeFileSync(ns + filepath, data, "utf8") } @@ -18,255 +20,255 @@ function remove(filepath) { o.spec("bundler", function() { o("relative imports works", function() { - write("a.js", `var b = require("./b")`) - write("b.js", `module.exports = 1`) + write("a.js", 'var b = require("./b")') + write("b.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b = 1\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports works with semicolons", function() { - write("a.js", `var b = require("./b");`) - write("b.js", `module.exports = 1;`) + write("a.js", 'var b = require("./b");') + write("b.js", "module.exports = 1;") bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\nvar b = 1;\n}`) - + o(read("out.js")).equals(";(function() {\nvar b = 1;\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports works with let", function() { - write("a.js", `let b = require("./b")`) - write("b.js", `module.exports = 1`) + write("a.js", 'let b = require("./b")') + write("b.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\nlet b = 1\n}`) - + o(read("out.js")).equals(";(function() {\nlet b = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports works with const", function() { write("a.js", 'const b = require("./b")') - write("b.js", `module.exports = 1`) + write("b.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\nconst b = 1\n}`) - + o(read("out.js")).equals(";(function() {\nconst b = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports works with assignment", function() { - write("a.js", `var a = {}\na.b = require("./b")`) - write("b.js", `module.exports = 1`) + write("a.js", 'var a = {}\na.b = require("./b")') + write("b.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar a = {}\na.b = 1\n}`) - + + o(read("out.js")).equals(";(function() {\nvar a = {}\na.b = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports works with reassignment", function() { - write("a.js", `var b = {}\nb = require("./b")`) - write("b.js", `module.exports = 1`) + write("a.js", 'var b = {}\nb = require("./b")') + write("b.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b = {}\nb = 1\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b = {}\nb = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports removes extra use strict", function() { - write("a.js", `"use strict"\nvar b = require("./b")`) - write("b.js", `"use strict"\nmodule.exports = 1`) + write("a.js", '"use strict"\nvar b = require("./b")') + write("b.js", '"use strict"\nmodule.exports = 1') bundle(ns + "a.js", ns + "out.js") - o(read("out.js")).equals(`new function() {\n"use strict"\nvar b = 1\n}`) - + o(read("out.js")).equals(';(function() {\n"use strict"\nvar b = 1\n}());') + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports removes extra use strict using single quotes", function() { - write("a.js", `'use strict'\nvar b = require("./b")`) - write("b.js", `'use strict'\nmodule.exports = 1`) + write("a.js", "'use strict'\nvar b = require(\"./b\")") + write("b.js", "'use strict'\nmodule.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\n'use strict'\nvar b = 1\n}`) - + + o(read("out.js")).equals(";(function() {\n'use strict'\nvar b = 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("relative imports removes extra use strict using mixed quotes", function() { - write("a.js", `"use strict"\nvar b = require("./b")`) - write("b.js", `'use strict'\nmodule.exports = 1`) + write("a.js", '"use strict"\nvar b = require("./b")') + write("b.js", "'use strict'\nmodule.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\n"use strict"\nvar b = 1\n}`) - + + o(read("out.js")).equals(';(function() {\n"use strict"\nvar b = 1\n}());') + remove("a.js") remove("b.js") remove("out.js") }) o("works w/ window", function() { - write("a.js", `window.a = 1\nvar b = require("./b")`) - write("b.js", `module.exports = function() {return a}`) + write("a.js", 'window.a = 1\nvar b = require("./b")') + write("b.js", "module.exports = function() {return a}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nwindow.a = 1\nvar b = function() {return a}\n}`) - + + o(read("out.js")).equals(";(function() {\nwindow.a = 1\nvar b = function() {return a}\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works without assignment", function() { - write("a.js", `require("./b")`) - write("b.js", `1 + 1`) + write("a.js", 'require("./b")') + write("b.js", "1 + 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\n1 + 1\n}`) - + + o(read("out.js")).equals(";(function() {\n1 + 1\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if used fluently", function() { - write("a.js", `var b = require("./b").toString()`) - write("b.js", `module.exports = []`) + write("a.js", 'var b = require("./b").toString()') + write("b.js", "module.exports = []") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar _0 = []\nvar b = _0.toString()\n}`) - + + o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if used fluently w/ multiline", function() { - write("a.js", `var b = require("./b")\n\t.toString()`) - write("b.js", `module.exports = []`) + write("a.js", 'var b = require("./b")\n\t.toString()') + write("b.js", "module.exports = []") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar _0 = []\nvar b = _0\n\t.toString()\n}`) - + + o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0\n\t.toString()\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if used w/ curry", function() { - write("a.js", `var b = require("./b")()`) - write("b.js", `module.exports = function() {}`) + write("a.js", 'var b = require("./b")()') + write("b.js", "module.exports = function() {}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar _0 = function() {}\nvar b = _0()\n}`) - + + o(read("out.js")).equals(";(function() {\nvar _0 = function() {}\nvar b = _0()\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if used w/ curry w/ multiline", function() { - write("a.js", `var b = require("./b")\n()`) - write("b.js", `module.exports = function() {}`) + write("a.js", 'var b = require("./b")\n()') + write("b.js", "module.exports = function() {}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar _0 = function() {}\nvar b = _0\n()\n}`) - + + o(read("out.js")).equals(";(function() {\nvar _0 = function() {}\nvar b = _0\n()\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if used fluently in one place and not in another", function() { - write("a.js", `var b = require("./b").toString()\nvar c = require("./c")`) - write("b.js", `module.exports = []`) - write("c.js", `var b = require("./b")\nmodule.exports = function() {return b}`) + write("a.js", 'var b = require("./b").toString()\nvar c = require("./c")') + write("b.js", "module.exports = []") + write("c.js", 'var b = require("./b")\nmodule.exports = function() {return b}') bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n}());") + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("works if used in sequence", function() { - write("a.js", `var b = require("./b"), c = require("./c")`) - write("b.js", `module.exports = 1`) - write("c.js", `var x\nmodule.exports = 2`) + write("a.js", 'var b = require("./b"), c = require("./c")') + write("b.js", "module.exports = 1") + write("c.js", "var x\nmodule.exports = 2") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b = 1\nvar x\nvar c = 2\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b = 1\nvar x\nvar c = 2\n}());") + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("works if assigned to property", function() { - write("a.js", `var x = {}\nx.b = require("./b")\nx.c = require("./c")`) - write("b.js", `var bb = 1\nmodule.exports = bb`) - write("c.js", `var cc = 2\nmodule.exports = cc`) + write("a.js", 'var x = {}\nx.b = require("./b")\nx.c = require("./c")') + write("b.js", "var bb = 1\nmodule.exports = bb") + write("c.js", "var cc = 2\nmodule.exports = cc") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}`) - + + o(read("out.js")).equals(";(function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}());") + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("works if assigned to property using bracket notation", function() { - write("a.js", `var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")`) - write("b.js", `var bb = 1\nmodule.exports = bb`) - write("c.js", `var cc = 2\nmodule.exports = cc`) + write("a.js", 'var x = {}\nx["b"] = require("./b")\nx["c"] = require("./c")') + write("b.js", "var bb = 1\nmodule.exports = bb") + write("c.js", "var cc = 2\nmodule.exports = cc") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n}`) - + + o(read("out.js")).equals(';(function() {\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n}());') + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("works if collision", function() { - write("a.js", `var b = require("./b")`) - write("b.js", `var b = 1\nmodule.exports = 2`) + write("a.js", 'var b = require("./b")') + write("b.js", "var b = 1\nmodule.exports = 2") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b0 = 1\nvar b = 2\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b0 = 1\nvar b = 2\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("works if multiple aliases", function() { - write("a.js", `var b = require("./b")\n`) - write("b.js", `var b = require("./c")\nb.x = 1\nmodule.exports = b`) - write("c.js", `var b = {}\nmodule.exports = b`) + write("a.js", 'var b = require("./b")\n') + write("b.js", 'var b = require("./c")\nb.x = 1\nmodule.exports = b') + write("c.js", "var b = {}\nmodule.exports = b") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b = {}\nb.x = 1\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b = {}\nb.x = 1\n}());") + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("works if multiple collision", function() { - write("a.js", `var b = require("./b")\nvar c = require("./c")\nvar d = require("./d")`) - write("b.js", `var a = 1\nmodule.exports = a`) - write("c.js", `var a = 2\nmodule.exports = a`) - write("d.js", `var a = 3\nmodule.exports = a`) + write("a.js", 'var b = require("./b")\nvar c = require("./c")\nvar d = require("./d")') + write("b.js", "var a = 1\nmodule.exports = a") + write("c.js", "var a = 2\nmodule.exports = a") + write("d.js", "var a = 3\nmodule.exports = a") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n}`) - + + o(read("out.js")).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = 2\nvar c = a0\nvar a1 = 3\nvar d = a1\n}());") + remove("a.js") remove("b.js") remove("c.js") @@ -274,38 +276,38 @@ o.spec("bundler", function() { 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")`) + 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}`) - + + o(read("out.js")).equals(";(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()`) + 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}`) - + + o(read("out.js")).equals(";(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}`) - write("c.js", `var d = require("./d")\nmodule.exports = function() {return d + 2}`) - write("d.js", `module.exports = 1`) + write("a.js", 'var b = require("./b")\nvar c = require("./c")') + write("b.js", 'var d = require("./d")\nmodule.exports = function() {return d + 1}') + write("c.js", 'var d = require("./d")\nmodule.exports = function() {return d + 2}') + write("d.js", "module.exports = 1") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n}());") + remove("a.js") remove("b.js") remove("c.js") @@ -313,71 +315,71 @@ o.spec("bundler", function() { remove("out.js") }) o("disambiguates conflicts if imported collides with itself", function() { - write("a.js", `var b = require("./b")`) - write("b.js", `var b = 1\nmodule.exports = function() {return b}`) + write("a.js", 'var b = require("./b")') + write("b.js", "var b = 1\nmodule.exports = function() {return b}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b0 = 1\nvar b = function() {return b0}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b0 = 1\nvar b = function() {return b0}\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("disambiguates conflicts if imported collides with something else", function() { - write("a.js", `var a = 1\nvar b = require("./b")`) - write("b.js", `var a = 2\nmodule.exports = function() {return a}`) + write("a.js", 'var a = 1\nvar b = require("./b")') + write("b.js", "var a = 2\nmodule.exports = function() {return a}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("disambiguates conflicts if imported collides with function declaration", function() { - write("a.js", `function a() {}\nvar b = require("./b")`) - write("b.js", `var a = 2\nmodule.exports = function() {return a}`) + write("a.js", 'function a() {}\nvar b = require("./b")') + write("b.js", "var a = 2\nmodule.exports = function() {return a}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n}`) - + + o(read("out.js")).equals(";(function() {\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n}());") + remove("a.js") remove("b.js") remove("out.js") }) o("disambiguates conflicts if imported collides with another module's private", function() { - write("a.js", `var b = require("./b")\nvar c = require("./c")`) - write("b.js", `var a = 1\nmodule.exports = function() {return a}`) - write("c.js", `var a = 2\nmodule.exports = function() {return a}`) + write("a.js", 'var b = require("./b")\nvar c = require("./c")') + write("b.js", "var a = 1\nmodule.exports = function() {return a}") + write("c.js", "var a = 2\nmodule.exports = function() {return a}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n}());") + remove("a.js") remove("b.js") remove("c.js") remove("out.js") }) o("does not mess up strings", function() { - write("a.js", `var b = require("./b")`) - write("b.js", `var b = "b b b \\\" b"\nmodule.exports = function() {return b}`) + write("a.js", 'var b = require("./b")') + write("b.js", 'var b = "b b b \\" b"\nmodule.exports = function() {return b}') bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}`) - + + o(read("out.js")).equals(';(function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}());') + remove("a.js") remove("b.js") remove("out.js") }) o("does not mess up properties", function() { - write("a.js", `var b = require("./b")`) - write("b.js", `var b = {b: 1}\nmodule.exports = function() {return b.b}`) + write("a.js", 'var b = require("./b")') + write("b.js", "var b = {b: 1}\nmodule.exports = function() {return b.b}") bundle(ns + "a.js", ns + "out.js") - - o(read("out.js")).equals(`new function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}`) - + + o(read("out.js")).equals(";(function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}());") + remove("a.js") remove("b.js") remove("out.js") }) -}) \ No newline at end of file +}) diff --git a/docs/components.md b/docs/components.md index c67ea89b..01721857 100644 --- a/docs/components.md +++ b/docs/components.md @@ -101,6 +101,107 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl --- +### Alternate component syntaxes + +#### ES6 classes + +Components can also be written using ES6 class syntax: + +```javascript +class ES6ClassComponent { + constructor(vnode) { + // vnode.state is undefined at this point + this.kind = "ES6 class" + } + view() { + return m("div", `Hello from an ${this.kind}`) + } + oncreate() { + console.log(`A ${this.kind} component was created`) + } +} +``` + +Component classes must define a `view()` method, detected via `.prototype.view`, to get the tree to render. + +They can be consumed in the same way regular components can. + +```javascript +// EXAMPLE: via m.render +m.render(document.body, m(ES6ClassComponent)) + +// EXAMPLE: via m.mount +m.mount(document.body, ES6ClassComponent) + +// EXAMPLE: via m.route +m.route(document.body, "/", { + "/": ES6ClassComponent +}) + +// EXAMPLE: component composition +class AnotherES6ClassComponent { + view() { + return m("main", [ + m(ES6ClassComponent) + ]) + } +} +``` + +#### Closure components + +Functionally minded developers may prefer using the "closure component" syntax: + +```javascript +function closureComponent(vnode) { + // vnode.state is undefined at this point + var kind = "closure component" + + return { + view: function() { + return m("div", "Hello from a " + kind) + }, + oncreate: function() { + console.log("We've created a " + kind) + } + } +} +``` + +The returned object must hold a `view` function, used to get the tree to render. + +They can be consumed in the same way regular components can. + +```javascript +// EXAMPLE: via m.render +m.render(document.body, m(closureComponent)) + +// EXAMPLE: via m.mount +m.mount(document.body, closuresComponent) + +// EXAMPLE: via m.route +m.route(document.body, "/", { + "/": closureComponent +}) + +// EXAMPLE: component composition +function anotherClosureComponent() { + return { + view: function() { + return m("main", [ + m(closureComponent) + ]) + } + } +} +``` + +#### Mixing component kinds + +Components can be freely mixed. A Class component can have closure or POJO components as children, etc... + +--- + ### State Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns. @@ -109,7 +210,7 @@ The state of a component can be accessed three ways: as a blueprint at initializ #### At initialization -The component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple state initialization. +For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple state initialization. In the example below, `data` is a property of the `ComponentWithInitialState` component's state object. @@ -127,6 +228,10 @@ m(ComponentWithInitialState) //
Initial content
``` +For class components, the state is an instance of the class, set right after the constructor is called. + +For closure components, the state is the object returned by the closure, set right after the closure returns. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead). + #### Via vnode.state State can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component. diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md index 4e4bd7f5..a27ae135 100644 --- a/docs/framework-comparison.md +++ b/docs/framework-comparison.md @@ -58,7 +58,7 @@ React | Mithril Library load times matter in applications that don't stay open for long periods of time (for example, anything in mobile) and cannot be improved via caching or other optimization techniques. -Since this is a micro-benchmark, you are encourage to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate. +Since this is a micro-benchmark, you are encouraged to replicate these tests yourself since hardware can heavily affect the numbers. Note that bundler frameworks like Webpack can move dependencies out before the timer calls to emulate static module resolution, so you should either copy the code from the compiled CDN files or open the output file from the bundler library, and manually add the high resolution timer calls `console.time` and `console.timeEnd` to the bundled script. Avoid using `new Date` and `performance.now`, as those mechanisms are not as statistically accurate. For your reading convenience, here's a version of that benchmark adapted to use CDNs on the web: the [benchmark for React is here](https://jsfiddle.net/0ovkv64u/), and the [benchmark for Mithril is here](https://jsfiddle.net/o7hxooqL/). Note that we're benchmarking all of Mithril rather than benchmarking only the rendering module (which would be equivalent in scope to React). Also note that this CDN-driven setup incurs some overheads due to fetching resources from disk cache (~2ms per resource). Due to those reasons, the numbers here are not entirely accurate, but they should be sufficient to observe that Mithril's initialization speed is noticeably better than React. diff --git a/docs/stream.md b/docs/stream.md index 250f3365..dc720ef4 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -5,6 +5,8 @@ - [Static members](#static-members) - [Stream.combine](#streamcombine) - [Stream.merge](#streammerge) + - [Stream.scan](#streamscan) + - [Stream.scanMerge](#streamscanmerge) - [Stream.HALT](#streamhalt) - [Stream["fantasy-land/of"]](#streamfantasy-landof) - [Instance members](#static-members) @@ -114,6 +116,39 @@ Argument | Type | Required | Description --- +##### Stream.scan + +Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value. + +`stream = Stream.scan(fn, accumulator, stream)` + +Argument | Type | Required | Description +------------- | -------------------------------- | -------- | --- +`fn` | `(accumulator, value) -> result` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value +`accumulator` | `any` | Yes | The starting value for the accumulator +`stream` | `Stream` | Yes | Stream containing the values +**returns** | `Stream` | | Returns a new stream containing the result + +[How to read signatures](signatures.md) + +--- + +##### Stream.scanMerge + +Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. + +`stream = Stream.scanMerge(pairs, accumulator)` + +Argument | Type | Required | Description +------------- | ------------------------------------------------ | -------- | --- +`pairs` | `Array<[Stream, (accumulator, value) -> value]>` | Yes | An array of tuples of stream and scan functions +`accumulator` | `any` | Yes | The starting value for the accumulator +**returns** | `Stream` | | Returns a new stream containing the result + +[How to read signatures](signatures.md) + +--- + ##### Stream.HALT A special value that can be returned to stream callbacks to halt execution of downstreams diff --git a/docs/vnodes.md b/docs/vnodes.md index 317b9c47..62330333 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -73,10 +73,11 @@ Property | Type | Description `text` | `(String|Number|Boolean)?` | This is used instead of `children` if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the `text` property even if they have a text node as their only child. `dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range. `domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property). -`state` | `Object`? | An object that is persisted between redraws. It is provided by the core engine when needed. In component vnodes, the `state` inherits prototypically from the component object/class. -`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use it. - - +`state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the `state` inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure. +`_state` | `Object?` | For components, a reference to the original `vnode.state` object, used to lookup the `view` and hooks. This property is only used internally by Mithril, do not use or modify it. +`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use or modify it. +`instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril, do not use or modify it. +`skip` | `Boolean` | This property is only used internally by Mithril when diffing keyed lists, do not use or modify it. --- @@ -87,7 +88,7 @@ The `tag` property of a vnode determines its type. There are five vnode types: Vnode type | Example | Description ------------ | ------------------------------ | --- Element | `{tag: "div"}` | Represents a DOM element. -Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. +Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. Text | `{tag: "#", children: ""}` | Represents a DOM text node. Trusted HTML | `{tag: "<", children: "
"}` | Represents a list of DOM elements from an HTML string. Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component. diff --git a/examples/todomvc/todomvc.js b/examples/todomvc/todomvc.js index 1f1f257a..83d2a822 100644 --- a/examples/todomvc/todomvc.js +++ b/examples/todomvc/todomvc.js @@ -38,7 +38,7 @@ var state = { update: function(title) { if (state.editing != null) { state.editing.title = title.trim() - if (state.editing.title === "") destroy(state.editing) + if (state.editing.title === "") state.destroy(state.editing) state.editing = null } }, @@ -104,7 +104,7 @@ var Todos = { m("label", {ondblclick: function() {state.dispatch("edit", [todo])}}, todo.title), m("button.destroy", {onclick: function() {state.dispatch("destroy", [todo])}}), ]), - m("input.edit", {onupdate: function(vnode) {ui.focus(vnode, todo)}, onkeypress: ui.save, onblur: ui.save}) + m("input.edit", {onupdate: function(vnode) {ui.focus(vnode, todo)}, onkeyup: ui.save, onblur: ui.save}) ]) }), ]), diff --git a/hyperscript.js b/hyperscript.js index 880a1594..16bf033a 100644 --- a/hyperscript.js +++ b/hyperscript.js @@ -1,3 +1,5 @@ +"use strict" + var hyperscript = require("./render/hyperscript") hyperscript.trust = require("./render/trust") diff --git a/mithril.js b/mithril.js index cf18e1ac..c3cf4c53 100644 --- a/mithril.js +++ b/mithril.js @@ -1,7 +1,7 @@ -new function() { - +;(function() { +"use strict" function Vnode(tag, key, attrs0, children, text, dom) { - return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) @@ -16,62 +16,82 @@ Vnode.normalizeChildren = function normalizeChildren(children) { } var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorCache = {} +var hasOwn = {}.hasOwnProperty +function compileSelector(selector) { + var match, tag = "div", classes = [], attrs = {} + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") tag = value + else if (type === "#") attrs.id = value + else if (type === ".") classes.push(value) + else if (match[3][0] === "[") { + var attrValue = match[6] + if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") + if (match[4] === "class") classes.push(attrValue) + else attrs[match[4]] = attrValue || true + } + } + if (classes.length > 0) attrs.className = classes.join(" ") + return selectorCache[selector] = {tag: tag, attrs: attrs} +} +function execSelector(state, attrs, children) { + var hasAttrs = false, childList, text + var className = attrs.className || attrs.class + for (var key in state.attrs) { + if (hasOwn.call(state.attrs, key)) { + attrs[key] = state.attrs[key] + } + } + if (className != null) { + if (attrs.class != null) { + attrs.class = undefined + attrs.className = className + } + if (state.attrs.className != null) { + attrs.className = state.attrs.className + " " + className + } + } + for (var key in attrs) { + if (hasOwn.call(attrs, key) && key !== "key") { + hasAttrs = true + break + } + } + if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") { + text = children[0].children + } else { + childList = children + } + return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text) +} function hyperscript(selector) { - if (selector == null || typeof selector !== "string" && typeof selector.view !== "function") { + // Because sloppy mode sucks + var attrs = arguments[1], start = 2, children + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { throw Error("The selector must be either a string or a component."); } - if (typeof selector === "string" && selectorCache[selector] === undefined) { - var match, tag, classes = [], attributes = {} - while (match = selectorParser.exec(selector)) { - var type = match[1], value = match[2] - if (type === "" && value !== "") tag = value - else if (type === "#") attributes.id = value - else if (type === ".") classes.push(value) - else if (match[3][0] === "[") { - var attrValue = match[6] - if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") - if (match[4] === "class") classes.push(attrValue) - else attributes[match[4]] = attrValue || true - } - } - if (classes.length > 0) attributes.className = classes.join(" ") - selectorCache[selector] = function(attrs, children) { - var hasAttrs = false, childList, text - var className = attrs.className || attrs.class - for (var key in attributes) attrs[key] = attributes[key] - if (className !== undefined) { - if (attrs.class !== undefined) { - attrs.class = undefined - attrs.className = className - } - if (attributes.className !== undefined) attrs.className = attributes.className + " " + className - } - for (var key in attrs) { - if (key !== "key") { - hasAttrs = true - break - } - } - if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children - else childList = children - return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined) - } + if (typeof selector === "string") { + var cached = selectorCache[selector] || compileSelector(selector) } - var attrs, children, childrenIndex - if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) { - attrs = arguments[1] - childrenIndex = 2 + if (!attrs) { + attrs = {} + } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { + attrs = {} + start = 1 } - else childrenIndex = 1 - if (arguments.length === childrenIndex + 1) { - children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]] - } - else { + if (arguments.length === start + 1) { + children = arguments[start] + if (!Array.isArray(children)) children = [children] + } else { children = [] - for (var i = childrenIndex; i < arguments.length; i++) children.push(arguments[i]) + while (start < arguments.length) children.push(arguments[start++]) + } + var normalized = Vnode.normalizeChildren(children) + if (typeof selector === "string") { + return execSelector(cached, attrs, normalized) + } else { + return Vnode(selector, attrs.key, attrs, normalized) } - if (typeof selector === "string") return selectorCache[selector](attrs || {}, Vnode.normalizeChildren(children)) - return Vnode(selector, attrs && attrs.key, attrs || {}, Vnode.normalizeChildren(children), undefined, undefined) } hyperscript.trust = function(html) { if (html == null) html = "" @@ -203,6 +223,7 @@ var buildQueryString = function(object) { else args.push(encodeURIComponent(key0) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) } } +var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i") var _8 = function($window, Promise) { var callbackCount = 0 var oncompletion @@ -238,14 +259,20 @@ var _8 = function($window, Promise) { var promise0 = new Promise(function(resolve, reject) { if (args.method == null) args.method = "GET" args.method = args.method.toUpperCase() - var useBody = typeof args.useBody === "boolean" ? args.useBody : args.method !== "GET" && args.method !== "TRACE" + var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true) if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify if (typeof args.deserialize !== "function") args.deserialize = deserialize if (typeof args.extract !== "function") args.extract = extract args.url = interpolate(args.url, args.data) if (useBody) args.data = args.serialize(args.data) else args.url = assemble(args.url, args.data) - var xhr = new $window.XMLHttpRequest() + var xhr = new $window.XMLHttpRequest(), + aborted = false, + _abort = xhr.abort + xhr.abort = function abort() { + aborted = true + _abort.call(xhr) + } xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) if (args.serialize === JSON.stringify && useBody) { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") @@ -259,12 +286,12 @@ var _8 = function($window, Promise) { } if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr xhr.onreadystatechange = function() { - // Don't throw errors on xhr.abort(). XMLHttpRequests ends up in a state of - // xhr.status == 0 and xhr.readyState == 4 if aborted after open, but before completion. - if (xhr.status && xhr.readyState === 4) { + // Don't throw errors on xhr.abort(). + if(aborted) return + if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { + if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { @@ -355,30 +382,33 @@ var coreRenderer = function($window) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - insertNode(parent, createNode(vnode, hooks, ns), nextSibling) + createNode(parent, vnode, hooks, ns, nextSibling) } } } - function createNode(vnode, hooks, ns) { + function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + vnode.state = {} + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { - case "#": return createText(vnode) - case "<": return createHTML(vnode) - case "[": return createFragment(vnode, hooks, ns) - default: return createElement(vnode, hooks, ns) + case "#": return createText(parent, vnode, nextSibling) + case "<": return createHTML(parent, vnode, nextSibling) + case "[": return createFragment(parent, vnode, hooks, ns, nextSibling) + default: return createElement(parent, vnode, hooks, ns, nextSibling) } } - else return createComponent(vnode, hooks, ns) + else return createComponent(parent, vnode, hooks, ns, nextSibling) } - function createText(vnode) { - return vnode.dom = $doc.createTextNode(vnode.children) + function createText(parent, vnode, nextSibling) { + vnode.dom = $doc.createTextNode(vnode.children) + insertNode(parent, vnode.dom, nextSibling) + return vnode.dom } - function createHTML(vnode) { + function createHTML(parent, vnode, nextSibling) { var match1 = vnode.children.match(/^\s*?<(\w+)/im) || [] - var parent = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match1[1]] || "div" - var temp = $doc.createElement(parent) + var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match1[1]] || "div" + var temp = $doc.createElement(parent1) temp.innerHTML = vnode.children vnode.dom = temp.firstChild vnode.domSize = temp.childNodes.length @@ -387,9 +417,10 @@ var coreRenderer = function($window) { while (child = temp.firstChild) { fragment.appendChild(child) } + insertNode(parent, fragment, nextSibling) return fragment } - function createFragment(vnode, hooks, ns) { + function createFragment(parent, vnode, hooks, ns, nextSibling) { var fragment = $doc.createDocumentFragment() if (vnode.children != null) { var children = vnode.children @@ -397,9 +428,10 @@ var coreRenderer = function($window) { } vnode.dom = fragment.firstChild vnode.domSize = fragment.childNodes.length + insertNode(parent, fragment, nextSibling) return fragment } - function createElement(vnode, hooks, ns) { + function createElement(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag switch (vnode.tag) { case "svg": ns = "http://www.w3.org/2000/svg"; break @@ -414,6 +446,7 @@ var coreRenderer = function($window) { if (attrs2 != null) { setAttrs(vnode, attrs2, ns) } + insertNode(parent, element, nextSibling) if (vnode.attrs != null && vnode.attrs.contenteditable != null) { setContentEditable(vnode) } @@ -430,19 +463,34 @@ var coreRenderer = function($window) { } return element } - function createComponent(vnode, hooks, ns) { - vnode.state = Object.create(vnode.tag) - var view = vnode.tag.view - if (view.reentrantLock != null) return $emptyFragment - view.reentrantLock = true - initLifecycle(vnode.tag, vnode, hooks) - vnode.instance = Vnode.normalize(view.call(vnode.state, vnode)) - view.reentrantLock = null + function initComponent(vnode, hooks) { + var sentinel + if (typeof vnode.tag.view === "function") { + vnode.state = Object.create(vnode.tag) + sentinel = vnode.state.view + if (sentinel.$$reentrantLock$$ != null) return $emptyFragment + sentinel.$$reentrantLock$$ = true + } else { + vnode.state = void 0 + sentinel = vnode.tag + if (sentinel.$$reentrantLock$$ != null) return $emptyFragment + sentinel.$$reentrantLock$$ = true + vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) + } + vnode._state = vnode.state + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + initLifecycle(vnode._state, vnode, hooks) + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") - var element = createNode(vnode.instance, hooks, ns) + var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 + insertNode(parent, element, nextSibling) return element } else { @@ -451,7 +499,7 @@ var coreRenderer = function($window) { } } //update - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) @@ -467,15 +515,18 @@ var coreRenderer = function($window) { if (isUnkeyed) { for (var i = 0; i < old.length; i++) { if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) + else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) + else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) } return } } - var recycling = isRecyclable(old, vnodes) - if (recycling) old = old.concat(old.pool) + recycling = recycling || isRecyclable(old, vnodes) + if (recycling) { + var pool = old.pool + old = old.concat(old.pool) + } var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map while (oldEnd >= oldStart && end >= start) { var o = old[oldStart], v = vnodes[start] @@ -483,8 +534,9 @@ var coreRenderer = function($window) { else if (o == null) oldStart++ else if (v == null) start++ else if (o.key === v.key) { + var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) } else { @@ -493,7 +545,8 @@ var coreRenderer = function($window) { else if (o == null) oldEnd-- else if (v == null) start++ else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) oldEnd--, start++ } @@ -506,7 +559,8 @@ var coreRenderer = function($window) { else if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- @@ -517,14 +571,14 @@ var coreRenderer = function($window) { var oldIndex = map[v.key] if (oldIndex != null) { var movable = old[oldIndex] + var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true if (movable.dom != null) nextSibling = movable.dom } else { - var dom = createNode(v, hooks, undefined) - insertNode(parent, dom, nextSibling) + var dom = createNode(parent, v, hooks, undefined, nextSibling) nextSibling = dom } } @@ -540,24 +594,29 @@ var coreRenderer = function($window) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state + vnode._state = old._state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) { + vnode.state = {} + initLifecycle(vnode.attrs, vnode, hooks) + } + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break - default: updateElement(old, vnode, hooks, ns) + case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break + default: updateElement(old, vnode, recycling, hooks, ns) } } else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) } else { removeNode(old, null) - insertNode(parent, createNode(vnode, hooks, ns), nextSibling) + createNode(parent, vnode, hooks, ns, nextSibling) } } function updateText(old, vnode) { @@ -569,12 +628,12 @@ var coreRenderer = function($window) { function updateHTML(parent, old, vnode, nextSibling) { if (old.children !== vnode.children) { toFragment(old) - insertNode(parent, createHTML(vnode), nextSibling) + createHTML(parent, vnode, nextSibling) } else vnode.dom = old.dom, vnode.domSize = old.domSize } - function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) + function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns) var domSize = 0, children = vnode.children vnode.dom = null if (children != null) { @@ -588,7 +647,7 @@ var coreRenderer = function($window) { if (domSize !== 1) vnode.domSize = domSize } } - function updateElement(old, vnode, hooks, ns) { + function updateElement(old, vnode, recycling, hooks, ns) { var element = vnode.dom = old.dom switch (vnode.tag) { case "svg": ns = "http://www.w3.org/2000/svg"; break @@ -611,14 +670,20 @@ var coreRenderer = function($window) { else { if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)] if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] - updateNodes(element, old.children, vnode.children, hooks, null, ns) + updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns) } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.tag.view.call(vnode.state, vnode)) - updateLifecycle(vnode.tag, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode._state, vnode, hooks) + } if (vnode.instance != null) { - if (old.instance == null) insertNode(parent, createNode(vnode.instance, hooks, ns), nextSibling) + if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) vnode.dom = vnode.instance.dom vnode.domSize = vnode.instance.domSize @@ -698,15 +763,15 @@ var coreRenderer = function($window) { } function removeNode(vnode, context) { var expected = 1, called = 0 - if (vnode.attrs && vnode.attrs.onbeforeremove) { + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) { - var result = vnode.tag.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { + var result = vnode._state.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -738,8 +803,8 @@ var coreRenderer = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && vnode.tag.onremove) vnode.tag.onremove.call(vnode.state, vnode) + if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children @@ -768,14 +833,14 @@ var coreRenderer = function($window) { else if (key2 === "style") updateStyle(element, old, value) else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "input" && key2 === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key2 === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "select" && key2 === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key2 === "value" && vnode.dom.value === value) return + if (vnode.tag === "option" && key2 === "value" && vnode.dom.value == value) return // If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error0 will occur. if (vnode.tag === "input" && key2 === "type") { - element.setAttribute(key2, value); + element.setAttribute(key2, value) return } element[key2] = value @@ -868,14 +933,13 @@ var coreRenderer = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode.tag.onbeforeupdate === "function") forceComponentUpdate = vnode.tag.onbeforeupdate.call(vnode.state, vnode, old) + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize @@ -891,7 +955,7 @@ var coreRenderer = function($window) { // First time0 rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" if (!Array.isArray(vnodes)) vnodes = [vnodes] - updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) + updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, undefined) dom.vnodes = vnodes for (var i = 0; i < hooks.length; i++) hooks[i]() if ($doc.activeElement !== active) active.focus() @@ -923,7 +987,6 @@ var _11 = function($window) { renderService.setEventCallback(function(e) { if (e.redraw !== false) redraw() }) - var callbacks = [] function subscribe(key1, callback) { unsubscribe(key1) @@ -933,11 +996,11 @@ var _11 = function($window) { var index = callbacks.indexOf(key1) if (index > -1) callbacks.splice(index, 2) } - function redraw() { - for (var i = 1; i < callbacks.length; i += 2) { - callbacks[i]() - } - } + function redraw() { + for (var i = 1; i < callbacks.length; i += 2) { + callbacks[i]() + } + } return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} } var redrawService = _11(window) @@ -950,7 +1013,7 @@ var _16 = function(redrawService0) { return } - if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") + if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode") var run0 = function() { redrawService0.render(root, Vnode(component)) @@ -1061,7 +1124,6 @@ var coreRouter = function($window) { var path = router.getPath() var params = {} var pathname = parsePath(path, params, params) - var state = $window.history.state if (state != null) { for (var k in state) params[k] = state[k] @@ -1082,12 +1144,10 @@ var coreRouter = function($window) { } reject(path, params) } - if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute resolveRoute() } - return router } var _20 = function($window, redrawService0) { @@ -1099,17 +1159,19 @@ var _20 = function($window, redrawService0) { var run1 = function() { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } - var bail = function() { - routeService.setPath(defaultRoute, null, {replace: true}) + var bail = function(path) { + if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) + else throw new Error("Could not resolve default route " + defaultRoute) } routeService.defineRoutes(routes, function(payload, params, path) { var update = lastUpdate = function(routeResolver, comp) { if (update !== lastUpdate) return - component = comp != null && typeof comp.view === "function" ? comp : "div", attrs3 = params, currentPath = path, lastUpdate = null + component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" + attrs3 = params, currentPath = path, lastUpdate = null render1 = (routeResolver.render || identity).bind(routeResolver) run1() } - if (payload.view) update({}, payload) + if (payload.view || typeof payload === "function") update({}, payload) else { if (payload.onmatch) { Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { @@ -1158,8 +1220,8 @@ m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.0.0" +m.version = "1.0.1" m.vnode = Vnode if (typeof module !== "undefined") module["exports"] = m else window.m = m -} \ No newline at end of file +}()); \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 8e7cd361..a7ddf4e9 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,42 +1,43 @@ -new function(){function x(a,c,k,d,h,m){return{tag:a,key:c,attrs:k,children:d,text:h,dom:m,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function B(a){if(null==a||"string"!==typeof a&&"function"!==typeof a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===G[a]){for(var c,k,d=[],h={};c=N.exec(a);){var m=c[1],l=c[2];""===m&&""!==l?k=l:"#"===m?h.id=l:"."===m?d.push(l):"["===c[3][0]&&((m=c[6])&&(m=m.replace(/\\(["'])/g,"$1").replace(/\\\\/g, -"\\")),"class"===c[4]?d.push(m):h[c[4]]=m||!0)}0b.indexOf("?")?"?":"&";b+=d+c}return b}function l(a){try{return""!==a?JSON.parse(a):null}catch(A){throw Error(a);}} -function p(a){return a.responseText}function r(a,c){if("function"===typeof a)if(Array.isArray(c))for(var b=0;bn.status||304===n.status)c(r(b.type,a));else{var g=Error(n.responseText),e;for(e in a)g[e]= -a[e];d(g)}}catch(f){d(f)}};k&&null!=b.data?n.send(b.data):n.send()});return!0===b.background?A:q(A)},jsonp:function(b,l){var p=k();b=d(b,l);var u=new c(function(c,d){var k=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+q++,l=a.document.createElement("script");a[k]=function(d){l.parentNode.removeChild(l);c(r(b.type,d));delete a[k]};l.onerror=function(){l.parentNode.removeChild(l);d(Error("JSONP request failed"));delete a[k]};null==b.data&&(b.data={});b.url=h(b.url,b.data);b.data[b.callbackKey|| -"callback"]=k;l.src=m(b.url,b.data);a.document.documentElement.appendChild(l)});return!0===b.background?u:p(u)},setCompletionCallback:function(a){u=a}}}(window,w),M=function(a){function c(g,e,a,b,c,d,h){for(;a=t&&A>=n;){var y=e[t],v=a[n];if(y!==v||f)if(null==y)t++;else if(null==v)n++;else if(y.key===v.key)t++,n++,m(g,y,v,b,p(e,t,d),f,h),f&&y.tag===v.tag&&r(g,l(y),d);else if(y=e[q],y!==v||f)if(null==y)q--;else if(null==v)n++;else if(y.key===v.key)m(g,y,v,b,p(e, -q+1,d),f,h),(f||n=t&&A>=n;){y=e[q];v=a[A];if(y!==v||f)if(null==y)q--;else{if(null!=v)if(y.key===v.key)m(g,y,v,b,p(e,q+1,d),f,h),f&&y.tag===v.tag&&r(g,l(y),d),null!=y.dom&&(d=y.dom),q--;else{if(!z){z=e;var y=q,C={},x;for(x=0;xa.indexOf("?")?"?":"&";a+=e+d}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a); +}}function q(a){return a.responseText}function m(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dl.status||304===l.status||S.test(a.url))d(m(a.type, +h));else{var c=Error(l.responseText),p;for(p in h)c[p]=h[p];f(c)}}catch(v){f(v)}};g&&null!=a.data?l.send(a.data):l.send()});return!0===a.background?w:u(w)},jsonp:function(a,k){var u=f();a=g(a,k);var q=new d(function(d,f){var g=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,k=b.document.createElement("script");b[g]=function(e){k.parentNode.removeChild(k);d(m(a.type,e));delete b[g]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete b[g]};null== +a.data&&(a.data={});a.url=e(a.url,a.data);a.data[a.callbackKey||"callback"]=g;k.src=n(a.url,a.data);b.document.documentElement.appendChild(k)});return!0===a.background?q:u(q)},setCompletionCallback:function(a){u=a}}}(window,x),O=function(b){function d(h,c,p,a,b,d,e){for(;p=v&&y>=t;){var r=c[v],z=p[t];if(r!==z||b)if(null==r)v++;else if(null==z)t++;else if(r.key===z.key){var A=null!=u&&v>=c.length-u.length||null==u&&b;v++;t++;k(h,r,z,e,m(c,v,g),A,n);b&&r.tag===z.tag&&l(h,q(r),g)}else if(r=c[w],r!==z||b)if(null==r)w--;else if(null==z)t++; +else if(r.key===z.key)A=null!=u&&w>=c.length-u.length||null==u&&b,k(h,r,z,e,m(c,w+1,g),A,n),(b||t=v&&y>=t;){r=c[w];z=p[y];if(r!==z||b)if(null==r)w--;else{if(null!=z)if(r.key===z.key)A=null!=u&&w>=c.length-u.length||null==u&&b,k(h,r,z,e,m(c,w+1,g),A,n),b&&r.tag===z.tag&&l(h,q(r),g),null!=r.dom&&(g=r.dom),w--;else{if(!G){G=c;var r=w,A={},E;for(E=0;E 0) attrs.className = classes.join(" ") + return selectorCache[selector] = {tag: tag, attrs: attrs} +} + +function execSelector(state, attrs, children) { + var hasAttrs = false, childList, text + var className = attrs.className || attrs.class + + for (var key in state.attrs) { + if (hasOwn.call(state.attrs, key)) { + attrs[key] = state.attrs[key] + } + } + + if (className != null) { + if (attrs.class != null) { + attrs.class = undefined + attrs.className = className + } + + if (state.attrs.className != null) { + attrs.className = state.attrs.className + " " + className + } + } + + for (var key in attrs) { + if (hasOwn.call(attrs, key) && key !== "key") { + hasAttrs = true + break + } + } + + if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") { + text = children[0].children + } else { + childList = children + } + + return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text) +} + function hyperscript(selector) { - if (selector == null || typeof selector !== "string" && typeof selector.view !== "function") { + // Because sloppy mode sucks + var attrs = arguments[1], start = 2, children + + if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { throw Error("The selector must be either a string or a component."); } - if (typeof selector === "string" && selectorCache[selector] === undefined) { - var match, tag, classes = [], attributes = {} - while (match = selectorParser.exec(selector)) { - var type = match[1], value = match[2] - if (type === "" && value !== "") tag = value - else if (type === "#") attributes.id = value - else if (type === ".") classes.push(value) - else if (match[3][0] === "[") { - var attrValue = match[6] - if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") - if (match[4] === "class") classes.push(attrValue) - else attributes[match[4]] = attrValue || true - } - } - if (classes.length > 0) attributes.className = classes.join(" ") - selectorCache[selector] = function(attrs, children) { - var hasAttrs = false, childList, text - var className = attrs.className || attrs.class - for (var key in attributes) attrs[key] = attributes[key] - if (className !== undefined) { - if (attrs.class !== undefined) { - attrs.class = undefined - attrs.className = className - } - if (attributes.className !== undefined) attrs.className = attributes.className + " " + className - } - for (var key in attrs) { - if (key !== "key") { - hasAttrs = true - break - } - } - if (Array.isArray(children) && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children - else childList = children + if (typeof selector === "string") { + var cached = selectorCache[selector] || compileSelector(selector) + } - return Vnode(tag || "div", attrs.key, hasAttrs ? attrs : undefined, childList, text, undefined) - } + if (!attrs) { + attrs = {} + } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { + attrs = {} + start = 1 } - var attrs, children, childrenIndex - if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !Array.isArray(arguments[1])) { - attrs = arguments[1] - childrenIndex = 2 - } - else childrenIndex = 1 - if (arguments.length === childrenIndex + 1) { - children = Array.isArray(arguments[childrenIndex]) ? arguments[childrenIndex] : [arguments[childrenIndex]] - } - else { + + if (arguments.length === start + 1) { + children = arguments[start] + if (!Array.isArray(children)) children = [children] + } else { children = [] - for (var i = childrenIndex; i < arguments.length; i++) children.push(arguments[i]) + while (start < arguments.length) children.push(arguments[start++]) } - if (typeof selector === "string") return selectorCache[selector](attrs || {}, Vnode.normalizeChildren(children)) + var normalized = Vnode.normalizeChildren(children) - return Vnode(selector, attrs && attrs.key, attrs || {}, Vnode.normalizeChildren(children), undefined, undefined) + if (typeof selector === "string") { + return execSelector(cached, attrs, normalized) + } else { + return Vnode(selector, attrs.key, attrs, normalized) + } } module.exports = hyperscript diff --git a/render/render.js b/render/render.js index 5508ae51..df26a1df 100644 --- a/render/render.js +++ b/render/render.js @@ -14,30 +14,33 @@ module.exports = function($window) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { - insertNode(parent, createNode(vnode, hooks, ns), nextSibling) + createNode(parent, vnode, hooks, ns, nextSibling) } } } - function createNode(vnode, hooks, ns) { + function createNode(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) if (typeof tag === "string") { + vnode.state = {} + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { - case "#": return createText(vnode) - case "<": return createHTML(vnode) - case "[": return createFragment(vnode, hooks, ns) - default: return createElement(vnode, hooks, ns) + case "#": return createText(parent, vnode, nextSibling) + case "<": return createHTML(parent, vnode, nextSibling) + case "[": return createFragment(parent, vnode, hooks, ns, nextSibling) + default: return createElement(parent, vnode, hooks, ns, nextSibling) } } - else return createComponent(vnode, hooks, ns) + else return createComponent(parent, vnode, hooks, ns, nextSibling) } - function createText(vnode) { - return vnode.dom = $doc.createTextNode(vnode.children) + function createText(parent, vnode, nextSibling) { + vnode.dom = $doc.createTextNode(vnode.children) + insertNode(parent, vnode.dom, nextSibling) + return vnode.dom } - function createHTML(vnode) { + function createHTML(parent, vnode, nextSibling) { var match = vnode.children.match(/^\s*?<(\w+)/im) || [] - var parent = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div" - var temp = $doc.createElement(parent) + var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div" + var temp = $doc.createElement(parent1) temp.innerHTML = vnode.children vnode.dom = temp.firstChild @@ -47,9 +50,10 @@ module.exports = function($window) { while (child = temp.firstChild) { fragment.appendChild(child) } + insertNode(parent, fragment, nextSibling) return fragment } - function createFragment(vnode, hooks, ns) { + function createFragment(parent, vnode, hooks, ns, nextSibling) { var fragment = $doc.createDocumentFragment() if (vnode.children != null) { var children = vnode.children @@ -57,9 +61,10 @@ module.exports = function($window) { } vnode.dom = fragment.firstChild vnode.domSize = fragment.childNodes.length + insertNode(parent, fragment, nextSibling) return fragment } - function createElement(vnode, hooks, ns) { + function createElement(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag switch (vnode.tag) { case "svg": ns = "http://www.w3.org/2000/svg"; break @@ -78,6 +83,8 @@ module.exports = function($window) { setAttrs(vnode, attrs, ns) } + insertNode(parent, element, nextSibling) + if (vnode.attrs != null && vnode.attrs.contenteditable != null) { setContentEditable(vnode) } @@ -94,19 +101,34 @@ module.exports = function($window) { } return element } - function createComponent(vnode, hooks, ns) { - vnode.state = Object.create(vnode.tag) - var view = vnode.tag.view - if (view.reentrantLock != null) return $emptyFragment - view.reentrantLock = true - initLifecycle(vnode.tag, vnode, hooks) - vnode.instance = Vnode.normalize(view.call(vnode.state, vnode)) - view.reentrantLock = null + function initComponent(vnode, hooks) { + var sentinel + if (typeof vnode.tag.view === "function") { + vnode.state = Object.create(vnode.tag) + sentinel = vnode.state.view + if (sentinel.$$reentrantLock$$ != null) return $emptyFragment + sentinel.$$reentrantLock$$ = true + } else { + vnode.state = void 0 + sentinel = vnode.tag + if (sentinel.$$reentrantLock$$ != null) return $emptyFragment + sentinel.$$reentrantLock$$ = true + vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) + } + vnode._state = vnode.state + if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) + initLifecycle(vnode._state, vnode, hooks) + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + sentinel.$$reentrantLock$$ = null + } + function createComponent(parent, vnode, hooks, ns, nextSibling) { + initComponent(vnode, hooks) if (vnode.instance != null) { - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as arguments") - var element = createNode(vnode.instance, hooks, ns) + var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 + insertNode(parent, element, nextSibling) return element } else { @@ -116,7 +138,7 @@ module.exports = function($window) { } //update - function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) @@ -132,15 +154,18 @@ module.exports = function($window) { if (isUnkeyed) { for (var i = 0; i < old.length; i++) { if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) insertNode(parent, createNode(vnodes[i], hooks, ns), getNextSibling(old, i + 1, nextSibling)) + else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), false, ns) + else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) } return } } - var recycling = isRecyclable(old, vnodes) - if (recycling) old = old.concat(old.pool) + recycling = recycling || isRecyclable(old, vnodes) + if (recycling) { + var pool = old.pool + old = old.concat(old.pool) + } var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map while (oldEnd >= oldStart && end >= start) { @@ -149,8 +174,9 @@ module.exports = function($window) { else if (o == null) oldStart++ else if (v == null) start++ else if (o.key === v.key) { + var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) } else { @@ -159,7 +185,8 @@ module.exports = function($window) { else if (o == null) oldEnd-- else if (v == null) start++ else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) oldEnd--, start++ } @@ -172,7 +199,8 @@ module.exports = function($window) { else if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) + var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) if (o.dom != null) nextSibling = o.dom oldEnd--, end-- @@ -183,14 +211,14 @@ module.exports = function($window) { var oldIndex = map[v.key] if (oldIndex != null) { var movable = old[oldIndex] + var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true if (movable.dom != null) nextSibling = movable.dom } else { - var dom = createNode(v, hooks, undefined) - insertNode(parent, dom, nextSibling) + var dom = createNode(parent, v, hooks, undefined, nextSibling) nextSibling = dom } } @@ -206,24 +234,29 @@ module.exports = function($window) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state + vnode._state = old._state vnode.events = old.events - if (shouldUpdate(vnode, old)) return - if (vnode.attrs != null) { - updateLifecycle(vnode.attrs, vnode, hooks, recycling) - } + if (!recycling && shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { + if (vnode.attrs != null) { + if (recycling) { + vnode.state = {} + initLifecycle(vnode.attrs, vnode, hooks) + } + else updateLifecycle(vnode.attrs, vnode, hooks) + } switch (oldTag) { case "#": updateText(old, vnode); break case "<": updateHTML(parent, old, vnode, nextSibling); break - case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break - default: updateElement(old, vnode, hooks, ns) + case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break + default: updateElement(old, vnode, recycling, hooks, ns) } } else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) } else { removeNode(old, null) - insertNode(parent, createNode(vnode, hooks, ns), nextSibling) + createNode(parent, vnode, hooks, ns, nextSibling) } } function updateText(old, vnode) { @@ -235,12 +268,12 @@ module.exports = function($window) { function updateHTML(parent, old, vnode, nextSibling) { if (old.children !== vnode.children) { toFragment(old) - insertNode(parent, createHTML(vnode), nextSibling) + createHTML(parent, vnode, nextSibling) } else vnode.dom = old.dom, vnode.domSize = old.domSize } - function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) + function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns) var domSize = 0, children = vnode.children vnode.dom = null if (children != null) { @@ -254,7 +287,7 @@ module.exports = function($window) { if (domSize !== 1) vnode.domSize = domSize } } - function updateElement(old, vnode, hooks, ns) { + function updateElement(old, vnode, recycling, hooks, ns) { var element = vnode.dom = old.dom switch (vnode.tag) { case "svg": ns = "http://www.w3.org/2000/svg"; break @@ -277,14 +310,20 @@ module.exports = function($window) { else { if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)] if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] - updateNodes(element, old.children, vnode.children, hooks, null, ns) + updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns) } } function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - vnode.instance = Vnode.normalize(vnode.tag.view.call(vnode.state, vnode)) - updateLifecycle(vnode.tag, vnode, hooks, recycling) + if (recycling) { + initComponent(vnode, hooks) + } else { + vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode._state, vnode, hooks) + } if (vnode.instance != null) { - if (old.instance == null) insertNode(parent, createNode(vnode.instance, hooks, ns), nextSibling) + if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) vnode.dom = vnode.instance.dom vnode.domSize = vnode.instance.domSize @@ -367,15 +406,15 @@ module.exports = function($window) { } function removeNode(vnode, context) { var expected = 1, called = 0 - if (vnode.attrs && vnode.attrs.onbeforeremove) { + if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && vnode.tag.onbeforeremove) { - var result = vnode.tag.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { + var result = vnode._state.onbeforeremove.call(vnode.state, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -407,8 +446,8 @@ module.exports = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && vnode.tag.onremove) vnode.tag.onremove.call(vnode.state, vnode) + if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) if (vnode.instance != null) onremove(vnode.instance) else { var children = vnode.children @@ -438,14 +477,14 @@ module.exports = function($window) { else if (key === "style") updateStyle(element, old, value) else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) { //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "input" && key === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key === "value" && vnode.dom.value === value && vnode.dom === $doc.activeElement) return + if (vnode.tag === "select" && key === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key === "value" && vnode.dom.value === value) return + if (vnode.tag === "option" && key === "value" && vnode.dom.value == value) return // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. if (vnode.tag === "input" && key === "type") { - element.setAttribute(key, value); + element.setAttribute(key, value) return } element[key] = value @@ -541,14 +580,13 @@ module.exports = function($window) { if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) } - function updateLifecycle(source, vnode, hooks, recycling) { - if (recycling) initLifecycle(source, vnode, hooks) - else if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + function updateLifecycle(source, vnode, hooks) { + if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) } - function shouldUpdate(vnode, old) { + function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode.tag.onbeforeupdate === "function") forceComponentUpdate = vnode.tag.onbeforeupdate.call(vnode.state, vnode, old) + if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize @@ -567,7 +605,7 @@ module.exports = function($window) { if (dom.vnodes == null) dom.textContent = "" if (!Array.isArray(vnodes)) vnodes = [vnodes] - updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) + updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, undefined) dom.vnodes = vnodes for (var i = 0; i < hooks.length; i++) hooks[i]() if ($doc.activeElement !== active) active.focus() diff --git a/render/tests/index.html b/render/tests/index.html index 480b8b7c..b978ae6f 100644 --- a/render/tests/index.html +++ b/render/tests/index.html @@ -8,6 +8,7 @@ + diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index 2d08007b..c6a3df31 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -12,19 +12,19 @@ o.spec("attributes", function() { render = vdom($window).render }) o.spec("customElements", function(){ - + o("when vnode is customElement, custom setAttribute called", function(){ - var normal = [ - { tag: "input", attrs: { value: 'hello' } }, - { tag: "input", attrs: { value: 'hello' } }, - { tag: "input", attrs: { value: 'hello' } } + var normal = [ + {tag: "input", attrs: {value: "hello"}}, + {tag: "input", attrs: {value: "hello"}}, + {tag: "input", attrs: {value: "hello"}} ] - + var custom = [ - { tag: "custom-element", attrs: { custom: 'x' } }, - { tag: "input", attrs: { is: 'something-special', custom: 'x' } }, - { tag: "custom-element", attrs: { is: 'something-special', custom: 'x' } } + {tag: "custom-element", attrs: {custom: "x"}}, + {tag: "input", attrs: {is: "something-special", custom: "x"}}, + {tag: "custom-element", attrs: {is: "something-special", custom: "x"}} ] var view = normal.concat(custom) @@ -43,8 +43,8 @@ o.spec("attributes", function() { } render(root, view) - - o(spy.callCount).equals( custom.length ) + + o(spy.callCount).equals(custom.length) }) }) @@ -133,7 +133,7 @@ o.spec("attributes", function() { }) o.spec("contenteditable throws on untrusted children", function() { o("including text nodes", function() { - var div = {tag: "div", attrs: {contenteditable: true}, text: ''} + var div = {tag: "div", attrs: {contenteditable: true}, text: ""} var succeeded = false try { @@ -141,7 +141,7 @@ o.spec("attributes", function() { succeeded = true } - catch(e){} + catch(e){/* ignore */} o(succeeded).equals(false) }) @@ -154,7 +154,7 @@ o.spec("attributes", function() { succeeded = true } - catch(e){} + catch(e){/* ignore */} o(succeeded).equals(false) }) @@ -167,7 +167,7 @@ o.spec("attributes", function() { succeeded = true } - catch(e){} + catch(e){/* ignore */} o(succeeded).equals(true) }) @@ -180,7 +180,7 @@ o.spec("attributes", function() { succeeded = true } - catch(e){} + catch(e){/* ignore */} o(succeeded).equals(true) }) diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 4bb0a3b6..94863236 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -9,689 +10,1022 @@ o.spec("component", function() { o.beforeEach(function() { $window = domMock() root = $window.document.createElement("div") + render = vdom($window).render }) - o.spec("basics", function() { - o("works", function() { - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - render(root, [node]) + o.spec("basics", function() { + o("works", function() { + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("receives arguments", function() { - var component = { - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs, text: vnode.text} - } - } - var node = {tag: component, attrs: {id: "a"}, text: "b"} + render(root, [node]) - render(root, [node]) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("receives arguments", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs, text: vnode.text} + } + }) + var node = {tag: component, attrs: {id: "a"}, text: "b"} - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("updates", function() { - var component = { - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs, text: vnode.text} - } - } - render(root, [{tag: component, attrs: {id: "a"}, text: "b"}]) - render(root, [{tag: component, attrs: {id: "c"}, text: "d"}]) + render(root, [node]) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("c") - o(root.firstChild.firstChild.nodeValue).equals("d") - }) - o("updates root from null", function() { - var visible = false - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : null - } - } - render(root, [{tag: component}]) - visible = true - render(root, [{tag: component}]) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("updates", function() { + var component = createComponent({ + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs, text: vnode.text} + } + }) + render(root, [{tag: component, attrs: {id: "a"}, text: "b"}]) + render(root, [{tag: component, attrs: {id: "c"}, text: "d"}]) - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root from primitive", function() { - var visible = false - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : false - } - } - render(root, [{tag: component}]) - visible = true - render(root, [{tag: component}]) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("c") + o(root.firstChild.firstChild.nodeValue).equals("d") + }) + o("updates root from null", function() { + var visible = false + var component = createComponent({ + view: function() { + return visible ? {tag: "div"} : null + } + }) + render(root, [{tag: component}]) + visible = true + render(root, [{tag: component}]) - o(root.firstChild.nodeName).equals("DIV") - }) - o("updates root to null", function() { - var visible = true - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : null - } - } - render(root, [{tag: component}]) - visible = false - render(root, [{tag: component}]) + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root from primitive", function() { + var visible = false + var component = createComponent({ + view: function() { + return visible ? {tag: "div"} : false + } + }) + render(root, [{tag: component}]) + visible = true + render(root, [{tag: component}]) - o(root.childNodes.length).equals(0) - }) - o("updates root to primitive", function() { - var visible = true - var component = { - view: function(vnode) { - return visible ? {tag: "div"} : false - } - } - render(root, [{tag: component}]) - visible = false - render(root, [{tag: component}]) + o(root.firstChild.nodeName).equals("DIV") + }) + o("updates root to null", function() { + var visible = true + var component = createComponent({ + view: function() { + return visible ? {tag: "div"} : null + } + }) + render(root, [{tag: component}]) + visible = false + render(root, [{tag: component}]) - o(root.firstChild.nodeValue).equals("") - }) - o("updates root from null to null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("removes", function() { - var component = { - view: function(vnode) { - return {tag: "div"} - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("svg works when creating across component boundary", function() { - var component = { - view: function(vnode) { - return {tag: "g"} - } - } - render(root, [{tag: "svg", children: [{tag: component}]}]) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - o("svg works when updating across component boundary", function() { - var component = { - view: function(vnode) { - return {tag: "g"} - } - } - render(root, [{tag: "svg", children: [{tag: component}]}]) - render(root, [{tag: "svg", children: [{tag: component}]}]) - - o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - }) - }) - o.spec("return value", function() { - o("can return fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can return string", function() { - var component = { - view: function(vnode) { - return "a" - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can return falsy string", function() { - var component = { - view: function(vnode) { - return "" - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return number", function() { - var component = { - view: function(vnode) { - return 1 - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("1") - }) - o("can return falsy number", function() { - var component = { - view: function(vnode) { - return 0 - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("0") - }) - o("can return boolean", function() { - var component = { - view: function(vnode) { - return true - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("true") - }) - o("can return falsy boolean", function() { - var component = { - view: function(vnode) { - return false - } - } - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("") - }) - o("can return null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("can return undefined", function() { - var component = { - view: function(vnode) { - return undefined - } - } - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("throws a custom error if it returns itself", function() { - // A view that returns its vnode would otherwise trigger an infinite loop - var component = { - view: function(vnode) { - return vnode - } - } - try { - render(root, [{tag: component}]) - } - catch (e) { - o(e instanceof Error).equals(true) - // Call stack exception is a RangeError - o(e instanceof RangeError).equals(false) - } - }) - o("can update when returning fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("LABEL") - o(root.childNodes[1].nodeName).equals("INPUT") - }) - o("can update when returning primitive", function() { - var component = { - view: function(vnode) { - return "a" - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.firstChild.nodeType).equals(3) - o(root.firstChild.nodeValue).equals("a") - }) - o("can update when returning null", function() { - var component = { - view: function(vnode) { - return null - } - } - render(root, [{tag: component}]) - render(root, [{tag: component}]) - - o(root.childNodes.length).equals(0) - }) - o("can remove when returning fragments", function() { - var component = { - view: function(vnode) { - return [ - {tag: "label"}, - {tag: "input"}, - ] - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - o("can remove when returning primitive", function() { - var component = { - view: function(vnode) { - return "a" - } - } - var div = {tag: "div", key: 2} - render(root, [{tag: component, key: 1}, div]) - - render(root, [{tag: "div", key: 2}]) - - o(root.childNodes.length).equals(1) - o(root.firstChild).equals(div.dom) - }) - }) - o.spec("lifecycle", function() { - o("calls oninit", function() { - var called = 0 - var component = { - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) o(root.childNodes.length).equals(0) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} + }) + o("updates root to primitive", function() { + var visible = true + var component = createComponent({ + view: function() { + return visible ? {tag: "div"} : false + } + }) + render(root, [{tag: component}]) + visible = false + render(root, [{tag: component}]) - render(root, [node]) + o(root.firstChild.nodeValue).equals("") + }) + o("updates root from null to null", function() { + var component = createComponent({ + view: function() { + return null + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit when returning fragment", function() { - var called = 0 - var component = { - oninit: function(vnode) { - called++ - - o(vnode.tag).equals(component) - o(vnode.dom).equals(undefined) o(root.childNodes.length).equals(0) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - var node = {tag: component} + }) + o("removes", function() { + var component = createComponent({ + view: function() { + return {tag: "div"} + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + render(root, [{tag: "div", key: 2}]) - render(root, [node]) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("svg works when creating across component boundary", function() { + var component = createComponent({ + view: function() { + return {tag: "g"} + } + }) + render(root, [{tag: "svg", children: [{tag: component}]}]) - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls oninit before view", function() { - var viewCalled = false + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("svg works when updating across component boundary", function() { + var component = createComponent({ + view: function() { + return {tag: "g"} + } + }) + render(root, [{tag: "svg", children: [{tag: component}]}]) + render(root, [{tag: "svg", children: [{tag: component}]}]) - render(root, { - tag: { - view: function() { - viewCalled = true - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - }, - oninit: function(vnode) { - o(viewCalled).equals(false) - }, - } + o(root.firstChild.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + }) + o.spec("return value", function() { + o("can return fragments", function() { + var component = createComponent({ + view: function() { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can return string", function() { + var component = createComponent({ + view: function() { + return "a" + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can return falsy string", function() { + var component = createComponent({ + view: function() { + return "" + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + }) + o("can return number", function() { + var component = createComponent({ + view: function() { + return 1 + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("1") + }) + o("can return falsy number", function() { + var component = createComponent({ + view: function() { + return 0 + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("0") + }) + o("can return boolean", function() { + var component = createComponent({ + view: function() { + return true + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("true") + }) + o("can return falsy boolean", function() { + var component = createComponent({ + view: function() { + return false + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + }) + o("can return null", function() { + var component = createComponent({ + view: function() { + return null + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("can return undefined", function() { + var component = createComponent({ + view: function() { + return undefined + } + }) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("throws a custom error if it returns itself when created", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false + var component = createComponent({ + view: function(vnode) { + return vnode + } + }) + try { + render(root, [{tag: component}]) + } + catch (e) { + threw = true + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + o(threw).equals(true) + }) + o("throws a custom error if it returns itself when updated", function() { + // A view that returns its vnode would otherwise trigger an infinite loop + var threw = false + var init = true + var oninit = o.spy() + var component = createComponent({ + oninit: oninit, + view: function(vnode) { + if (init) return init = false + else return vnode + } + }) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("") + + try { + render(root, [{tag: component}]) + } + catch (e) { + threw = true + o(e instanceof Error).equals(true) + // Call stack exception is a RangeError + o(e instanceof RangeError).equals(false) + } + o(threw).equals(true) + o(oninit.callCount).equals(1) + }) + o("can update when returning fragments", function() { + var component = createComponent({ + view: function() { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("LABEL") + o(root.childNodes[1].nodeName).equals("INPUT") + }) + o("can update when returning primitive", function() { + var component = createComponent({ + view: function() { + return "a" + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.firstChild.nodeType).equals(3) + o(root.firstChild.nodeValue).equals("a") + }) + o("can update when returning null", function() { + var component = createComponent({ + view: function() { + return null + } + }) + render(root, [{tag: component}]) + render(root, [{tag: component}]) + + o(root.childNodes.length).equals(0) + }) + o("can remove when returning fragments", function() { + var component = createComponent({ + view: function() { + return [ + {tag: "label"}, + {tag: "input"}, + ] + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + + render(root, [{tag: "div", key: 2}]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + o("can remove when returning primitive", function() { + var component = createComponent({ + view: function() { + return "a" + } + }) + var div = {tag: "div", key: 2} + render(root, [{tag: component, key: 1}, div]) + + render(root, [{tag: "div", key: 2}]) + + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(div.dom) + }) + }) + o.spec("lifecycle", function() { + o("calls oninit", function() { + var called = 0 + var component = createComponent({ + oninit: function(vnode) { + called++ + + o(vnode.tag).equals(component) + o(vnode.dom).equals(undefined) + o(root.childNodes.length).equals(0) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls oninit when returning fragment", function() { + var called = 0 + var component = createComponent({ + oninit: function(vnode) { + called++ + + o(vnode.tag).equals(component) + o(vnode.dom).equals(undefined) + o(root.childNodes.length).equals(0) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls oninit before view", function() { + var viewCalled = false + + render(root, createComponent({ + tag: { + view: function() { + viewCalled = true + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + }, + oninit: function() { + o(viewCalled).equals(false) + }, + } + })) + }) + o("does not calls oninit on redraw", function() { + var init = o.spy() + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + }, + oninit: init, + }) + + function view() { + return {tag: component} + } + + render(root, view()) + render(root, view()) + + o(init.callCount).equals(1) + }) + o("calls oncreate", function() { + var called = 0 + var component = createComponent({ + oncreate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("does not calls oncreate on redraw", function() { + var create = o.spy() + var component = createComponent({ + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + }, + oncreate: create, + }) + + function view() { + return {tag: component} + } + + render(root, view()) + render(root, view()) + + o(create.callCount).equals(1) + }) + o("calls oncreate when returning fragment", function() { + var called = 0 + var component = createComponent({ + oncreate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + var node = {tag: component} + + render(root, [node]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onupdate", function() { + var called = 0 + var component = createComponent({ + onupdate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, [{tag: component}]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onupdate when returning fragment", function() { + var called = 0 + var component = createComponent({ + onupdate: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, [{tag: component}]) + + o(called).equals(1) + o(root.firstChild.nodeName).equals("DIV") + o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.firstChild.nodeValue).equals("b") + }) + o("calls onremove", function() { + var called = 0 + var component = createComponent({ + onremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onremove when returning fragment", function() { + var called = 0 + var component = createComponent({ + onremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onbeforeremove", function() { + var called = 0 + var component = createComponent({ + onbeforeremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return {tag: "div", attrs: {id: "a"}, text: "b"} + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("calls onbeforeremove when returning fragment", function() { + var called = 0 + var component = createComponent({ + onbeforeremove: function(vnode) { + called++ + + o(vnode.dom).notEquals(undefined) + o(vnode.dom).equals(root.firstChild) + o(root.childNodes.length).equals(1) + }, + view: function() { + return [{tag: "div", attrs: {id: "a"}, text: "b"}] + } + }) + + render(root, [{tag: component}]) + + o(called).equals(0) + + render(root, []) + + o(called).equals(1) + o(root.childNodes.length).equals(0) + }) + o("does not recycle when there's an onupdate", function() { + var component = createComponent({ + onupdate: function() {}, + view: function() { + return {tag: "div"} + } + }) + var vnode = {tag: component, key: 1} + var updated = {tag: component, key: 1} + + render(root, [vnode]) + render(root, []) + render(root, [updated]) + + o(vnode.dom).notEquals(updated.dom) + }) + o("lifecycle timing megatest (for a single component)", function() { + var methods = { + view: o.spy(function() { + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + // the `attrs` hooks are called before the component ones + attrs[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount + 1) + }) + methods[hook] = o.spy(function() { + o(attrs[hook].callCount).equals(methods[hook].callCount) + }) + }) + + var component = createComponent(methods) + + o(methods.view.callCount).equals(0) + o(methods.oninit.callCount).equals(0) + o(methods.oncreate.callCount).equals(0) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(1) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, []) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(1) + o(methods.onremove.callCount).equals(1) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + }) + o("lifecycle timing megatest (for a single component with the state overwritten)", function() { + var methods = { + view: o.spy(function(vnode) { + o(vnode.state).equals(1) + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + // the `attrs` hooks are called before the component ones + attrs[hook] = o.spy(function(vnode) { + o(vnode.state).equals(1) + o(attrs[hook].callCount).equals(methods[hook].callCount + 1) + }) + methods[hook] = o.spy(function(vnode) { + o(vnode.state).equals(1) + o(attrs[hook].callCount).equals(methods[hook].callCount) + }) + }) + + var attrsOninit = attrs.oninit + var methodsOninit = methods.oninit + attrs.oninit = o.spy(function(vnode){ + vnode.state = 1 + return attrsOninit.call(this, vnode) + }) + methods.oninit = o.spy(function(vnode){ + vnode.state = 1 + return methodsOninit.call(this, vnode) + }) + + var component = createComponent(methods) + + o(methods.view.callCount).equals(0) + o(methods.oninit.callCount).equals(0) + o(methods.oncreate.callCount).equals(0) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(1) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(0) + o(methods.onupdate.callCount).equals(0) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, [{tag: component, attrs: attrs}]) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(0) + o(methods.onremove.callCount).equals(0) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + + render(root, []) + + o(methods.view.callCount).equals(2) + o(methods.oninit.callCount).equals(1) + o(methods.oncreate.callCount).equals(1) + o(methods.onbeforeupdate.callCount).equals(1) + o(methods.onupdate.callCount).equals(1) + o(methods.onbeforeremove.callCount).equals(1) + o(methods.onremove.callCount).equals(1) + + hooks.forEach(function(hook) { + o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) + }) + }) + o("hook state and arguments validation", function(){ + var methods = { + view: o.spy(function(vnode) { + o(this).equals(vnode.state) + return "" + }) + } + var attrs = {} + var hooks = [ + "oninit", "oncreate", "onbeforeupdate", + "onupdate", "onbeforeremove", "onremove" + ] + hooks.forEach(function(hook) { + attrs[hook] = o.spy(function(vnode){ + o(this).equals(vnode.state)(hook) + }) + methods[hook] = o.spy(function(vnode){ + o(this).equals(vnode.state) + }) + }) + + var component = createComponent(methods) + + render(root, [{tag: component, attrs: attrs}]) + render(root, [{tag: component, attrs: attrs}]) + render(root, []) + + hooks.forEach(function(hook) { + o(attrs[hook].this).equals(methods.view.this)(hook) + o(methods[hook].this).equals(methods.view.this)(hook) + }) + + o(methods.view.args.length).equals(1) + o(methods.oninit.args.length).equals(1) + o(methods.oncreate.args.length).equals(1) + o(methods.onbeforeupdate.args.length).equals(2) + o(methods.onupdate.args.length).equals(1) + o(methods.onbeforeremove.args.length).equals(1) + o(methods.onremove.args.length).equals(1) + + hooks.forEach(function(hook) { + o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) + }) + }) + o("recycled components get a fresh state", function() { + var step = 0 + var firstState + var view = o.spy(function(vnode) { + if (step === 0) { + firstState = vnode.state + } else { + o(vnode.state).notEquals(firstState) + } + return {tag: "div"} + }) + var component = createComponent({view: view}) + + render(root, [{tag: "div", children: [{tag: component, key: 1}]}]) + var child = root.firstChild.firstChild + render(root, []) + step = 1 + render(root, [{tag: "div", children: [{tag: component, key: 1}]}]) + + o(child).equals(root.firstChild.firstChild) + o(view.callCount).equals(2) + }) + }) + o.spec("state", function() { + o("initializes state", function() { + var data = {a: 1} + var component = createComponent(createComponent({ + data: data, + oninit: init, + view: function() { + return "" + } + })) + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + } + }) + o("state proxies to the component object/prototype", function() { + var body = {a: 1} + var data = [body] + var component = createComponent(createComponent({ + data: data, + oninit: init, + view: function() { + return "" + } + })) + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + o(vnode.state.data[0]).equals(body) + } + }) }) }) - o("does not calls oninit on redraw", function() { - var init = o.spy() - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - }, - oninit: init, - } - - function view() { - return {tag: component} - } - - render(root, view()) - render(root, view()) - - o(init.callCount).equals(1) - }) - o("calls oncreate", function() { - var called = 0 - var component = { - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("does not calls oncreate on redraw", function() { - var create = o.spy() - var component = { - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - }, - oncreate: create, - } - - function view() { - return {tag: component} - } - - render(root, view()) - render(root, view()) - - o(create.callCount).equals(1) - }) - o("calls oncreate when returning fragment", function() { - var called = 0 - var component = { - oncreate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - var node = {tag: component} - - render(root, [node]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate", function() { - var called = 0 - var component = { - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, [{tag: component}]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onupdate when returning fragment", function() { - var called = 0 - var component = { - onupdate: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, [{tag: component}]) - - o(called).equals(1) - o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") - o(root.firstChild.firstChild.nodeValue).equals("b") - }) - o("calls onremove", function() { - var called = 0 - var component = { - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onremove when returning fragment", function() { - var called = 0 - var component = { - onremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove", function() { - var called = 0 - var component = { - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return {tag: "div", attrs: {id: "a"}, text: "b"} - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("calls onbeforeremove when returning fragment", function() { - var called = 0 - var component = { - onbeforeremove: function(vnode) { - called++ - - o(vnode.dom).notEquals(undefined) - o(vnode.dom).equals(root.firstChild) - o(root.childNodes.length).equals(1) - }, - view: function() { - return [{tag: "div", attrs: {id: "a"}, text: "b"}] - } - } - - render(root, [{tag: component}]) - - o(called).equals(0) - - render(root, []) - - o(called).equals(1) - o(root.childNodes.length).equals(0) - }) - o("does not recycle when there's an onupdate", function() { - var component = { - onupdate: function() {}, - view: function() { - return {tag: "div"} - } - } - var update = o.spy() - var vnode = {tag: component, key: 1} - var updated = {tag: component, key: 1} - - render(root, [vnode]) - render(root, []) - render(root, [updated]) - - o(vnode.dom).notEquals(updated.dom) - }) }) - o.spec("state", function() { - o("copies state", function() { - var called = 0 - var data = {a: 1} - var component = { - data: data, - oninit: init, - view: function() { - return "" + o.spec("Tests specific to certain component kinds", function() { + o.spec("state", function() { + o("POJO", function() { + var data = {} + var component = { + data: data, + oninit: init, + view: function() { + return "" + } } - } - render(root, [{tag: component}]) + render(root, [{tag: component}]) - function init(vnode) { - o(vnode.state.data).deepEquals(data) - o(vnode.state.data).equals(data) + function init(vnode) { + o(vnode.state.data).equals(data) - //inherits state via prototype - component.x = 1 - o(vnode.state.x).equals(1) - } - }) - o("state copy is shallow", function() { - var called = 0 - var body = {a: 1} - var data = [body] - var component = { - data: data, - oninit: init, - view: function() { - return "" + //inherits state via prototype + component.x = 1 + o(vnode.state.x).equals(1) } - } + }) + o("Constructible", function() { + var oninit = o.spy() + var component = o.spy(function(vnode){ + o(vnode.state).equals(undefined) + o(oninit.callCount).equals(0) + }) + var view = o.spy(function(){ + o(this instanceof component).equals(true) + return "" + }) + component.prototype.view = view + component.prototype.oninit = oninit - render(root, [{tag: component}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) - function init(vnode) { - o(vnode.state.data).equals(data) - o(vnode.state.data[0]).equals(body) - } + o(component.callCount).equals(1) + o(oninit.callCount).equals(2) + o(view.callCount).equals(2) + }) + o("Closure", function() { + var state + var oninit = o.spy() + var view = o.spy(function() { + o(this).equals(state) + return "" + }) + var component = o.spy(function(vnode) { + o(vnode.state).equals(undefined) + o(oninit.callCount).equals(0) + return state = { + view: view + } + }) + + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, [{tag: component, attrs: {oninit: oninit}}]) + render(root, []) + + o(component.callCount).equals(1) + o(oninit.callCount).equals(1) + o(view.callCount).equals(2) + }) }) }) }) diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js index 31599cfd..9e2827cf 100644 --- a/render/tests/test-createElement.js +++ b/render/tests/test-createElement.js @@ -1,3 +1,4 @@ +/* eslint-disable no-script-url */ "use strict" var o = require("../../ospec/ospec") diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 32e767d8..2264fe18 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -1,3 +1,5 @@ +"use strict" + var o = require("../../ospec/ospec") var m = require("../../render/hyperscript") @@ -424,7 +426,7 @@ o.spec("hyperscript", function() { }) }) o.spec("components", function() { - o("works", function() { + o("works with POJOs", function() { var component = { view: function() { return m("div") @@ -432,6 +434,19 @@ o.spec("hyperscript", function() { } var vnode = m(component, {id: "a"}, "b") + o(vnode.tag).equals(component) + o(vnode.attrs.id).equals("a") + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("b") + }) + o("works with functions", function() { + var component = o.spy() + + var vnode = m(component, {id: "a"}, "b") + + o(component.callCount).equals(0) + o(vnode.tag).equals(component) o(vnode.attrs.id).equals("a") o(vnode.children.length).equals(1) diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index c9af4894..2e23215b 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -2,6 +2,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var Promise = require("../../promise/promise") @@ -16,7 +17,6 @@ o.spec("onbeforeremove", function() { o("does not call onbeforeremove when creating", function() { var create = o.spy() - var update = o.spy() var vnode = {tag: "div", attrs: {onbeforeremove: create}} render(root, [vnode]) @@ -141,7 +141,7 @@ o.spec("onbeforeremove", function() { o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) }) o("does not recycle when there's an onbeforeremove", function() { - var remove = function(vnode) {} + var remove = function() {} var vnode = {tag: "div", key: 1, attrs: {onbeforeremove: remove}} var updated = {tag: "div", key: 1, attrs: {onbeforeremove: remove}} @@ -152,7 +152,7 @@ o.spec("onbeforeremove", function() { o(vnode.dom).notEquals(updated.dom) }) o("does not leave elements out of order during removal", function(done) { - var remove = function(vnode) {return Promise.resolve()} + var remove = function() {return Promise.resolve()} var vnodes = [{tag: "div", key: 1, attrs: {onbeforeremove: remove}, text: "1"}, {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"}] var updated = {tag: "div", key: 2, attrs: {onbeforeremove: remove}, text: "2"} @@ -169,39 +169,44 @@ o.spec("onbeforeremove", function() { done() }) }) - o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) { - var onremove = o.spy() - var onbeforeremove = function(){return Promise.resolve()} - var component = { - onbeforeremove: onbeforeremove, - onremove: onremove, - view: function() {}, - } - render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}]) - render(root, []) - callAsync(function() { - o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` - done() - }) - }) - o("awaits promise resolution before removing the node", function(done) { - var view = o.spy() - var onremove = o.spy() - var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} - var component = { - onbeforeremove: onbeforeremove, - onremove: onremove, - view: view, - } - render(root, [{tag: component}]) - render(root, []) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + o("finalizes the remove phase asynchronously when promise is returned synchronously from both attrs- and tag.onbeforeremove", function(done) { + var onremove = o.spy() + var onbeforeremove = function(){return Promise.resolve()} + var component = createComponent({ + onbeforeremove: onbeforeremove, + onremove: onremove, + view: function() {}, + }) + render(root, [{tag: component, attrs: {onbeforeremove: onbeforeremove, onremove: onremove}}]) + render(root, []) + callAsync(function() { + o(onremove.callCount).equals(2) // once for `tag`, once for `attrs` + done() + }) + }) + o("awaits promise resolution before removing the node", function(done) { + var view = o.spy() + var onremove = o.spy() + var onbeforeremove = function(){return new Promise(function(resolve){callAsync(resolve)})} + var component = createComponent({ + onbeforeremove: onbeforeremove, + onremove: onremove, + view: view, + }) + render(root, [{tag: component}]) + render(root, []) - callAsync(function(){ - o(onremove.callCount).equals(0) + callAsync(function(){ + o(onremove.callCount).equals(0) - callAsync(function() { - o(onremove.callCount).equals(1) - done() + callAsync(function() { + o(onremove.callCount).equals(1) + done() + }) + }) }) }) }) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 8b326b8c..9814122b 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -56,86 +57,6 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.nodeValue).equals("a") }) - o("prevents update in component", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", children: vnode.children} - }, - } - var vnode = {tag: component, children: [{tag: "#", children: "a"}]} - var updated = {tag: component, children: [{tag: "#", children: "b"}]} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.firstChild.nodeValue).equals("a") - }) - - o("prevents update if returning false in component and false in vnode", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("a") - }) - - o("does not prevent update if returning true in component and true in vnode", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - - o("does not prevent update if returning false in component but true in vnode", function() { - var component = { - onbeforeupdate: function() {return false}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - - o("does not prevent update if returning true in component but false in vnode", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: {id: vnode.attrs.id}} - }, - } - var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} - var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("does not prevent update if returning true", function() { var onbeforeupdate = function() {return true} var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -147,22 +68,6 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.attributes["id"].nodeValue).equals("b") }) - o("does not prevent update if returning true from component", function() { - var component = { - onbeforeupdate: function() {return true}, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} - - render(root, [vnode]) - render(root, [updated]) - - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("accepts arguments for comparison", function() { var count = 0 var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} @@ -184,63 +89,13 @@ o.spec("onbeforeupdate", function() { o(root.firstChild.attributes["id"].nodeValue).equals("b") }) - o("accepts arguments for comparison in component", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - var count = 0 - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} - - render(root, [vnode]) - render(root, [updated]) - - function onbeforeupdate(vnode, old) { - count++ - - o(old.attrs.id).equals("a") - o(vnode.attrs.id).equals("b") - - return old.attrs.id !== vnode.attrs.id - } - - o(count).equals(1) - o(root.firstChild.attributes["id"].nodeValue).equals("b") - }) - o("is not called on creation", function() { var count = 0 var vnode = {tag: "div", attrs: {id: "a", onbeforeupdate: onbeforeupdate}} - var updated = {tag: "div", attrs: {id: "b", onbeforeupdate: onbeforeupdate}} render(root, [vnode]) - function onbeforeupdate(vnode, old) { - count++ - return true - } - - o(count).equals(0) - }) - - o("is not called on component creation", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } - - var count = 0 - var vnode = {tag: "div", attrs: {id: "a"}} - var updated = {tag: "div", attrs: {id: "b"}} - - render(root, [vnode]) - - function onbeforeupdate(vnode, old) { + function onbeforeupdate() { count++ return true } @@ -256,7 +111,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - function onbeforeupdate(vnode, old) { + function onbeforeupdate() { count++ return true } @@ -264,26 +119,191 @@ o.spec("onbeforeupdate", function() { o(count).equals(1) }) - o("is called only once on component update", function() { - var component = { - onbeforeupdate: onbeforeupdate, - view: function(vnode) { - return {tag: "div", attrs: vnode.attrs} - }, - } + o("doesn't fire on recycled nodes", function() { + var onbeforeupdate = o.spy() + var vnodes = [{tag: "div", key: 1}] + var temp = [] + var updated = [{tag: "div", key: 1, attrs: {onbeforeupdate: onbeforeupdate}}] - var count = 0 - var vnode = {tag: component, attrs: {id: "a"}} - var updated = {tag: component, attrs: {id: "b"}} + render(root, vnodes) + render(root, temp) + render(root, updated) - render(root, [vnode]) - render(root, [updated]) + o(vnodes[0].dom).equals(updated[0].dom) + o(updated[0].dom.nodeName).equals("DIV") + o(onbeforeupdate.callCount).equals(0) + }) - function onbeforeupdate(vnode, old) { - count++ - return true - } + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create - o(count).equals(1) + o("prevents update in component", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", children: vnode.children} + }, + }) + var vnode = {tag: component, children: [{tag: "#", children: "a"}]} + var updated = {tag: component, children: [{tag: "#", children: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.firstChild.nodeValue).equals("a") + }) + + o("prevents update if returning false in component and false in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("a") + }) + + o("does not prevent update if returning true in component and true in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning false in component but true in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return false}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return true}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return true}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning true in component but false in vnode", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: {id: vnode.attrs.id}} + }, + }) + var vnode = {tag: component, attrs: {id: "a", onbeforeupdate: function() {return false}}} + var updated = {tag: component, attrs: {id: "b", onbeforeupdate: function() {return false}}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("does not prevent update if returning true from component", function() { + var component = createComponent({ + onbeforeupdate: function() {return true}, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("accepts arguments for comparison in component", function() { + var component = createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + var count = 0 + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + function onbeforeupdate(vnode, old) { + count++ + + o(old.attrs.id).equals("a") + o(vnode.attrs.id).equals("b") + + return old.attrs.id !== vnode.attrs.id + } + + o(count).equals(1) + o(root.firstChild.attributes["id"].nodeValue).equals("b") + }) + + o("is not called on component creation", function() { + createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + + var count = 0 + var vnode = {tag: "div", attrs: {id: "a"}} + + render(root, [vnode]) + + function onbeforeupdate() { + count++ + return true + } + + o(count).equals(0) + }) + + o("is called only once on component update", function() { + var component = createComponent({ + onbeforeupdate: onbeforeupdate, + view: function(vnode) { + return {tag: "div", attrs: vnode.attrs} + }, + }) + + var count = 0 + var vnode = {tag: component, attrs: {id: "a"}} + var updated = {tag: component, attrs: {id: "b"}} + + render(root, [vnode]) + render(root, [updated]) + + function onbeforeupdate() { + count++ + return true + } + + o(count).equals(1) + }) + }) }) }) diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js index d6f275c2..eb1daa5e 100644 --- a/render/tests/test-oncreate.js +++ b/render/tests/test-oncreate.js @@ -128,7 +128,6 @@ o.spec("oncreate", function() { }) o("does not call oncreate when removing", function() { var create = o.spy() - var update = o.spy() var vnode = {tag: "div", attrs: {oncreate: create}, state: {}} render(root, [vnode]) diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index 1f0e7701..4d94cae4 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -128,7 +128,6 @@ o.spec("oninit", function() { }) o("does not call oninit when removing", function() { var create = o.spy() - var update = o.spy() var vnode = {tag: "div", attrs: {oninit: create}, state: {}} render(root, [vnode]) @@ -187,7 +186,7 @@ o.spec("oninit", function() { called = true o(vnode.dom).equals(undefined) - o(root.childNodes.length).equals(0) + o(root.childNodes.length).equals(1) } o(called).equals(true) }) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index d3f423fc..a7f88a6b 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") var m = require("../../render/hyperscript") @@ -80,39 +81,6 @@ o.spec("onremove", function() { o(remove.this).equals(vnode.state) o(remove.args[0]).equals(vnode) }) - o("calls onremove on nested component", function() { - var spy = o.spy() - var comp = { - view: function() {return m(outer)} - } - var outer = { - view: function() {return m(inner)} - } - var inner = { - onremove: spy, - view: function() {return m("div")} - } - render(root, {tag: comp}) - render(root, null) - - o(spy.callCount).equals(1) - }) - o("calls onremove on nested component child", function() { - var spy = o.spy() - var comp = { - view: function() {return m(outer)} - } - var outer = { - view: function() {return m(inner, m("a", {onremove: spy}))} - } - var inner = { - view: function(vnode) {return m("div", vnode.children)} - } - render(root, {tag: comp}) - render(root, null) - - o(spy.callCount).equals(1) - }) o("does not set onremove as an event handler", function() { var remove = o.spy() var vnode = {tag: "div", attrs: {onremove: remove}, children: []} @@ -145,4 +113,43 @@ o.spec("onremove", function() { o(vnode.dom).notEquals(updated.dom) }) -}) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("calls onremove on nested component", function() { + var spy = o.spy() + var comp = createComponent({ + view: function() {return m(outer)} + }) + var outer = createComponent({ + view: function() {return m(inner)} + }) + var inner = createComponent({ + onremove: spy, + view: function() {return m("div")} + }) + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) + o("calls onremove on nested component child", function() { + var spy = o.spy() + var comp = createComponent({ + view: function() {return m(outer)} + }) + var outer = createComponent({ + view: function() {return m(inner, m("a", {onremove: spy}))} + }) + var inner = createComponent({ + view: function(vnode) {return m("div", vnode.children)} + }) + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 448a44f0..13f62c46 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -59,7 +59,6 @@ o.spec("onupdate", function() { }) o("does not call old onupdate when removing the onupdate property in new vnode", function() { var create = o.spy() - var update = o.spy() var vnode = {tag: "a", attrs: {onupdate: create}} var updated = {tag: "a"} diff --git a/render/tests/test-render.js b/render/tests/test-render.js index daa094fd..92bcd93c 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -21,7 +21,7 @@ o.spec("render", function() { o(root.childNodes.length).equals(0) }) - + o("throws on invalid root node", function() { var threw = false try { @@ -31,8 +31,8 @@ o.spec("render", function() { } o(threw).equals(true) }) - - o("does not enter infinite loop when oninit triggers render and view throws", function(done) { + + o("does not enter infinite loop when oninit triggers render and view throws with an object literal component", function(done) { var A = { oninit: init, view: function() {throw new Error("error")} @@ -44,15 +44,231 @@ o.spec("render", function() { setTimeout(function() { var threwInner = false try {run()} catch (e) {threwInner = true} - + o(threwInner).equals(false) done() }, 0) } - + var threwOuter = false try {run()} catch (e) {threwOuter = true} - + o(threwOuter).equals(true) }) + o("does not try to re-initialize a constructibe component whose view has thrown", function() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A(){} + A.prototype.view = function() {throw new Error("error")} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a constructible component whose oninit has thrown", function() { + var oninit = o.spy(function(){throw new Error("error")}) + var onbeforeupdate = o.spy() + function A(){} + A.prototype.view = function(){} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a constructible component whose constructor has thrown", function() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A(){throw new Error("error")} + A.prototype.view = function() {} + A.prototype.oninit = oninit + A.prototype.onbeforeupdate = onbeforeupdate + var throwCount = 0 + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(0) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(0) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a closure component whose view has thrown", function() { + var oninit = o.spy() + var onbeforeupdate = o.spy() + function A() { + return { + view: function() {throw new Error("error")}, + oninit: oninit, + onbeforeupdate: onbeforeupdate + } + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a closure component whose oninit has thrown", function() { + var oninit = o.spy(function() {throw new Error("error")}) + var onbeforeupdate = o.spy() + function A() { + return { + view: function() {}, + oninit: oninit, + onbeforeupdate: onbeforeupdate + } + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + o(oninit.callCount).equals(1) + o(onbeforeupdate.callCount).equals(0) + }) + o("does not try to re-initialize a closure component whose closure has thrown", function() { + function A() { + throw new Error("error") + } + var throwCount = 0 + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + + try {render(root, {tag: A})} catch (e) {throwCount++} + + o(throwCount).equals(1) + }) + o("lifecycle methods work in keyed children of recycled keyed", function() { + var createA = o.spy() + var updateA = o.spy() + var removeA = o.spy() + var createB = o.spy() + var updateB = o.spy() + var removeB = o.spy() + var a = function() { + return {tag: "div", key: 1, children: [ + {tag: "div", key: 11, attrs: {oncreate: createA, onupdate: updateA, onremove: removeA}}, + {tag: "div", key: 12} + ]} + } + var b = function() { + return {tag: "div", key: 2, children: [ + {tag: "div", key: 21, attrs: {oncreate: createB, onupdate: updateB, onremove: removeB}}, + {tag: "div", key: 22} + ]} + } + render(root, a()) + render(root, b()) + render(root, a()) + + o(createA.callCount).equals(2) + o(updateA.callCount).equals(0) + o(removeA.callCount).equals(1) + o(createB.callCount).equals(1) + o(updateB.callCount).equals(0) + o(removeB.callCount).equals(1) + }) + o("lifecycle methods work in unkeyed children of recycled keyed", function() { + var createA = o.spy() + var updateA = o.spy() + var removeA = o.spy() + var createB = o.spy() + var updateB = o.spy() + var removeB = o.spy() + var a = function() { + return {tag: "div", key: 1, children: [ + {tag: "div", attrs: {oncreate: createA, onupdate: updateA, onremove: removeA}}, + ]} + } + var b = function() { + return {tag: "div", key: 2, children: [ + {tag: "div", attrs: {oncreate: createB, onupdate: updateB, onremove: removeB}}, + ]} + } + render(root, a()) + render(root, b()) + render(root, a()) + + o(createA.callCount).equals(2) + o(updateA.callCount).equals(0) + o(removeA.callCount).equals(1) + o(createB.callCount).equals(1) + o(updateB.callCount).equals(0) + o(removeB.callCount).equals(1) + }) + o("update lifecycle methods work on children of recycled keyed", function() { + var createA = o.spy() + var updateA = o.spy() + var removeA = o.spy() + var createB = o.spy() + var updateB = o.spy() + var removeB = o.spy() + + var a = function() { + return {tag: "div", key: 1, children: [ + {tag: "div", attrs: {oncreate: createA, onupdate: updateA, onremove: removeA}}, + ]} + } + var b = function() { + return {tag: "div", key: 2, children: [ + {tag: "div", attrs: {oncreate: createB, onupdate: updateB, onremove: removeB}}, + ]} + } + render(root, a()) + render(root, a()) + o(createA.callCount).equals(1) + o(updateA.callCount).equals(1) + o(removeA.callCount).equals(0) + + render(root, b()) + o(createA.callCount).equals(1) + o(updateA.callCount).equals(1) + o(removeA.callCount).equals(1) + + render(root, a()) + render(root, a()) + + o(createA.callCount).equals(2) + o(updateA.callCount).equals(2) + o(removeA.callCount).equals(1) + }) }) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 2211efd1..1774e61f 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -213,10 +213,10 @@ o.spec("updateElement", function() { }) o("updates svg child", function() { var vnode = {tag: "svg", children: [{ - tag: 'circle' + tag: "circle" }]} var updated = {tag: "svg", children: [{ - tag: 'line' + tag: "line" }]} render(root, [vnode]) @@ -235,7 +235,7 @@ o.spec("updateElement", function() { render(root, [vnode]) var c = vnode.dom - + o(root.childNodes.length).equals(1) o(a).equals(c) }) @@ -254,7 +254,7 @@ o.spec("updateElement", function() { render(root, [e, b, f]) var y = root.childNodes[1] - + o(root.childNodes.length).equals(3) o(x).equals(y) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index b0e8c337..fa5231ea 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -1,6 +1,7 @@ "use strict" var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") var vdom = require("../../render/render") @@ -726,7 +727,7 @@ o.spec("updateNodes", function() { }) o("change type, position and length", function() { var vnodes = {tag: "div", children: [ - undefined, + undefined, {tag: "#", children: "a"} ]} var updated = {tag: "div", children: [ @@ -737,7 +738,7 @@ o.spec("updateNodes", function() { render(root, vnodes) render(root, updated) - + o(root.firstChild.childNodes.length).equals(1) }) o("removes then recreates then reverses children", function() { @@ -838,38 +839,6 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) - o("fragment child toggles from null when followed by null component then tag", function() { - var component = {view: function() {return null}} - var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] - var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}] - var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] - - render(root, vnodes) - render(root, temp) - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("B") - }) - o("fragment child toggles from null in component when followed by null component then tag", function() { - var flag = true - var a = {view: function() {return flag ? {tag: "a"} : null}} - var b = {view: function() {return null}} - var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] - - render(root, vnodes) - flag = false - render(root, temp) - flag = true - render(root, updated) - - o(root.childNodes.length).equals(2) - o(root.childNodes[0].nodeName).equals("A") - o(root.childNodes[1].nodeName).equals("S") - }) o("cached, non-keyed nodes skip diff", function () { var onupdate = o.spy(); var cached = {tag:"a", attrs:{onupdate: onupdate}} @@ -895,13 +864,13 @@ o.spec("updateNodes", function() { var vnodes = [{tag: "div"}, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] var temp = [null, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] var updated = [{tag: "div"}, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] - + render(root, vnodes) var before = vnodes[1].dom render(root, temp) render(root, updated) var after = updated[1].dom - + o(before).equals(after) o(create.callCount).equals(1) o(update.callCount).equals(2) @@ -914,26 +883,63 @@ o.spec("updateNodes", function() { var vnodes = [{tag: "b"}, {tag: "div"}, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] var temp = [{tag: "b"}, null, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] var updated = [{tag: "b"}, {tag: "div"}, {tag: "a", attrs: {oncreate: create, onupdate: update, onremove: remove}}] - + render(root, vnodes) var before = vnodes[2].dom render(root, temp) render(root, updated) var after = updated[2].dom - + o(before).equals(after) o(create.callCount).equals(1) o(update.callCount).equals(2) o(remove.callCount).equals(0) }) - o("component is recreated if key changes to undefined", function () { + o("node is recreated if key changes to undefined", function () { var vnode = {tag: "b", key: 1} var updated = {tag: "b"} - + render(root, vnode) - var dom = vnode.dom render(root, updated) - + o(vnode.dom).notEquals(updated.dom) }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o("fragment child toggles from null when followed by null component then tag", function() { + var component = createComponent({view: function() {return null}}) + var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}] + var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("fragment child toggles from null in component when followed by null component then tag", function() { + var flag = true + var a = createComponent({view: function() {return flag ? {tag: "a"} : null}}) + var b = createComponent({view: function() {return null}}) + var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + + render(root, vnodes) + flag = false + render(root, temp) + flag = true + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("S") + }) + }) + }) }) diff --git a/render/vnode.js b/render/vnode.js index 56df8c81..ce137703 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,5 +1,7 @@ +"use strict" + function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) diff --git a/request.js b/request.js index 7e86421a..cb3bfbbe 100644 --- a/request.js +++ b/request.js @@ -1,2 +1,4 @@ +"use strict" + var PromisePolyfill = require("./promise/promise") module.exports = require("./request/request")(window, PromisePolyfill) diff --git a/request/request.js b/request/request.js index 7b1b4b41..76e218c9 100644 --- a/request/request.js +++ b/request/request.js @@ -45,7 +45,7 @@ module.exports = function($window, Promise) { if (args.method == null) args.method = "GET" args.method = args.method.toUpperCase() - var useBody = typeof args.useBody === "boolean" ? args.useBody : args.method !== "GET" && args.method !== "TRACE" + var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true) if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify if (typeof args.deserialize !== "function") args.deserialize = deserialize diff --git a/request/tests/test-jsonp.js b/request/tests/test-jsonp.js index 7042274c..076ab26e 100644 --- a/request/tests/test-jsonp.js +++ b/request/tests/test-jsonp.js @@ -7,7 +7,7 @@ var Promise = require("../../promise/promise") var parseQueryString = require("../../querystring/parse") o.spec("jsonp", function() { - var mock, jsonp, spy, complete + var mock, jsonp, complete o.beforeEach(function() { mock = xhrMock() var requestService = Request(mock, Promise) @@ -28,7 +28,6 @@ o.spec("jsonp", function() { }).then(done) }) o("first argument can be a string aliasing url property", function(done){ - var s = new Date mock.$defineRoutes({ "GET /item": function(request) { var queryData = parseQueryString(request.query) @@ -104,8 +103,8 @@ o.spec("jsonp", function() { return {status: 200, responseText: queryData["callback"] + "([])"} } }) - var promise = jsonp("/item", {background: true}).then(function() {}) - + jsonp("/item", {background: true}).then(function() {}) + setTimeout(function() { o(complete.callCount).equals(0) done() diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 273d592c..7f965498 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -18,7 +18,6 @@ o.spec("xhr", function() { o.spec("success", function() { o("works via GET", function(done) { - var s = new Date mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} @@ -31,7 +30,6 @@ o.spec("xhr", function() { }) }) o("implicit GET method", function(done){ - var s = new Date mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} @@ -44,7 +42,6 @@ o.spec("xhr", function() { }) }) o("first argument can be a string aliasing url property", function(done){ - var s = new Date mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} @@ -172,7 +169,7 @@ o.spec("xhr", function() { } mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])} } }) @@ -186,7 +183,7 @@ o.spec("xhr", function() { } mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: JSON.stringify({id: 1})} } }) @@ -228,12 +225,12 @@ o.spec("xhr", function() { } mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: JSON.stringify({test: 123})} } }) xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals("{\"test\":123}") + o(data).equals('{"test":123}') }).then(done) }) o("deserialize parameter works in POST", function(done) { @@ -242,40 +239,40 @@ o.spec("xhr", function() { } mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: JSON.stringify({test: 123})} } }) xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals("{\"test\":123}") + o(data).equals('{"test":123}') }).then(done) }) o("extract parameter works in GET", function(done) { - var extract = function(data) { + var extract = function() { return JSON.stringify({test: 123}) } mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: ""} } }) xhr({method: "GET", url: "/item", extract: extract}).then(function(data) { - o(data).equals("{\"test\":123}") + o(data).equals('{"test":123}') }).then(done) }) o("extract parameter works in POST", function(done) { - var extract = function(data) { + var extract = function() { return JSON.stringify({test: 123}) } mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: ""} } }) xhr({method: "POST", url: "/item", extract: extract}).then(function(data) { - o(data).equals("{\"test\":123}") + o(data).equals('{"test":123}') }).then(done) }) o("ignores deserialize if extract is defined", function(done) { @@ -285,7 +282,7 @@ o.spec("xhr", function() { var deserialize = o.spy() mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: ""} } }) @@ -297,7 +294,7 @@ o.spec("xhr", function() { }) o("config parameter works", function(done) { mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: ""} } }) @@ -311,7 +308,7 @@ o.spec("xhr", function() { }) o("requests don't block each other", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: "[]"} } }) @@ -328,7 +325,7 @@ o.spec("xhr", function() { }) o("requests trigger finally once with a chained then", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: "[]"} } }) @@ -342,11 +339,11 @@ o.spec("xhr", function() { }) o("requests does not trigger finally when background: true", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 200, responseText: "[]"} } }) - var promise = xhr("/item", {background: true}).then(function() {}) + xhr("/item", {background: true}).then(function() {}) setTimeout(function() { o(complete.callCount).equals(0) @@ -355,7 +352,7 @@ o.spec("xhr", function() { }) o("headers are set when header arg passed", function(done) { mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: ""} } }) @@ -367,7 +364,7 @@ o.spec("xhr", function() { }) o("headers are with higher precedence than default headers", function(done) { mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: ""} } }) @@ -379,7 +376,7 @@ o.spec("xhr", function() { }) o("json headers are set to the correct default value", function(done) { mock.$defineRoutes({ - "POST /item": function(request) { + "POST /item": function() { return {status: 200, responseText: ""} } }) @@ -391,7 +388,6 @@ o.spec("xhr", function() { } }) o("doesn't fail on abort", function(done) { - var s = new Date mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} @@ -410,9 +406,9 @@ o.spec("xhr", function() { done() }, 0) } - Object.defineProperty(xhr, 'onreadystatechange', { - set: function(val) { onreadystatechange = val } - , get: function() { return testonreadystatechange } + Object.defineProperty(xhr, "onreadystatechange", { + set: function(val) { onreadystatechange = val }, + get: function() { return testonreadystatechange } }) xhr.abort() } @@ -424,7 +420,6 @@ o.spec("xhr", function() { }) }) o("doesn't fail on file:// status 0", function(done) { - var s = new Date mock.$defineRoutes({ "GET /item": function() { return {status: 0, responseText: JSON.stringify({a: 1})} @@ -456,7 +451,7 @@ o.spec("xhr", function() { o.spec("failure", function() { o("rejects on server error", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 500, responseText: JSON.stringify({error: "error"})} } }) @@ -467,7 +462,7 @@ o.spec("xhr", function() { }) o("extends Error with JSON response", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})} } }) @@ -479,7 +474,7 @@ o.spec("xhr", function() { }) o("rejects on non-JSON server error", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 500, responseText: "error"} } }) @@ -489,7 +484,7 @@ o.spec("xhr", function() { }) o("triggers all branched catches upon rejection", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 500, responseText: "error"} } }) @@ -515,7 +510,7 @@ o.spec("xhr", function() { }) o("rejects on cors-like error", function(done) { mock.$defineRoutes({ - "GET /item": function(request) { + "GET /item": function() { return {status: 0} } }) diff --git a/route.js b/route.js index b8c8a15a..4e829527 100644 --- a/route.js +++ b/route.js @@ -1,3 +1,5 @@ +"use strict" + var redrawService = require("./redraw") -module.exports = require("./api/router")(window, redrawService) \ No newline at end of file +module.exports = require("./api/router")(window, redrawService) diff --git a/router/router.js b/router/router.js index 51961e99..106f6863 100644 --- a/router/router.js +++ b/router/router.js @@ -80,7 +80,7 @@ module.exports = function($window) { var path = router.getPath() var params = {} var pathname = parsePath(path, params, params) - + var state = $window.history.state if (state != null) { for (var k in state) params[k] = state[k] @@ -103,11 +103,11 @@ module.exports = function($window) { reject(path, params) } - + if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute resolveRoute() } - + return router } diff --git a/router/tests/test-getPath.js b/router/tests/test-getPath.js index 6c5dc2b2..9b2603fa 100644 --- a/router/tests/test-getPath.js +++ b/router/tests/test-getPath.js @@ -6,7 +6,7 @@ var Router = require("../../router/router") o.spec("Router.getPath", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { - void ["#", "?", "", "#!", "?!", '/foo'].forEach(function(prefix) { + void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { var $window, router, onRouteChange, onFail diff --git a/stream.js b/stream.js index 06dcd5d4..89e5baab 100644 --- a/stream.js +++ b/stream.js @@ -1 +1,3 @@ -module.exports = require("./stream/stream") \ No newline at end of file +"use strict" + +module.exports = require("./stream/stream") diff --git a/stream/package.json b/stream/package.json new file mode 100644 index 00000000..f48972d5 --- /dev/null +++ b/stream/package.json @@ -0,0 +1,13 @@ +{ + "name": "mithril-stream", + "version": "1.0.0", + "description": "Streaming data, mithril-style", + "main": "stream.js", + "directories": { + "test": "tests" + }, + "keywords": [ "stream", "reactive", "data" ], + "author": "Leo Horie ", + "license": "MIT", + "repository": "lhorie/mithril.js" +} diff --git a/stream/scan.js b/stream/scan.js deleted file mode 100644 index 84b6e0a2..00000000 --- a/stream/scan.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict" - -var combine = require("./stream").combine - -module.exports = function (reducer, seed, stream) { - var newStream = combine(function (s) { - return seed = reducer(seed, s._state.value) - }, [stream]) - - if (newStream._state.state === 0) newStream(seed) - - return newStream -} diff --git a/stream/scanMerge.js b/stream/scanMerge.js deleted file mode 100644 index 088440fa..00000000 --- a/stream/scanMerge.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict" - -var combine = require("./stream").combine - -module.exports = function(tuples, seed) { - var streams = tuples.map(function(tuple) { - var stream = tuple[0] - if (stream._state.state === 0) stream(undefined) - return stream - }) - - var newStream = combine(function() { - var changed = arguments[arguments.length - 1] - - streams.forEach(function(stream, idx) { - if (changed.indexOf(stream) > -1) { - seed = tuples[idx][1](seed, stream._state.value) - } - }) - - return seed - }, streams) - - return newStream -} diff --git a/stream/stream.js b/stream/stream.js index 4a51f9e2..18c7e608 100644 --- a/stream/stream.js +++ b/stream/stream.js @@ -1,5 +1,7 @@ "use strict" +;(function() { + var guid = 0, HALT = {} function createStream() { function stream() { @@ -14,7 +16,7 @@ function createStream() { } function initStream(stream) { stream.constructor = createStream - stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined} + stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined, unregister: undefined} stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf @@ -23,7 +25,10 @@ function initStream(stream) { if (!stream._state.endStream) { var endStream = createStream() endStream.map(function(value) { - if (value === true) unregisterStream(stream), unregisterStream(endStream) + if (value === true) { + unregisterStream(stream) + endStream._state.unregister = function(){unregisterStream(endStream)} + } return value }) stream._state.endStream = endStream @@ -35,6 +40,7 @@ function initStream(stream) { function updateStream(stream, value) { updateState(stream, value) for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false) + if (stream._state.unregister != null) stream._state.unregister() finalize(stream) } function updateState(stream, value) { @@ -56,7 +62,7 @@ function finalize(stream) { } function combine(fn, streams) { - if (!streams.every(valid)) throw new Error("Ensure that each item passed to m.prop.combine/m.prop.merge is a stream") + if (!streams.every(valid)) throw new Error("Ensure that each item passed to stream.combine/stream.merge is a stream") return initDependency(createStream(), streams, function() { return fn.apply(this, streams.concat([streams.filter(changed)])) }) @@ -107,10 +113,48 @@ function merge(streams) { return streams.map(function(s) {return s()}) }, streams) } + +function scan(reducer, seed, stream) { + var newStream = combine(function (s) { + return seed = reducer(seed, s._state.value) + }, [stream]) + + if (newStream._state.state === 0) newStream(seed) + + return newStream +} + +function scanMerge(tuples, seed) { + var streams = tuples.map(function(tuple) { + var stream = tuple[0] + if (stream._state.state === 0) stream(undefined) + return stream + }) + + var newStream = combine(function() { + var changed = arguments[arguments.length - 1] + + streams.forEach(function(stream, idx) { + if (changed.indexOf(stream) > -1) { + seed = tuples[idx][1](seed, stream._state.value) + } + }) + + return seed + }, streams) + + return newStream +} + createStream["fantasy-land/of"] = createStream createStream.merge = merge createStream.combine = combine +createStream.scan = scan +createStream.scanMerge = scanMerge createStream.HALT = HALT if (typeof module !== "undefined") module["exports"] = createStream -else window.stream = createStream +else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = createStream +else window.m = {stream : createStream} + +}()); diff --git a/stream/tests/test-scan.js b/stream/tests/test-scan.js index 72ef0277..db5d885f 100644 --- a/stream/tests/test-scan.js +++ b/stream/tests/test-scan.js @@ -2,12 +2,11 @@ var o = require("../../ospec/ospec") var stream = require("../stream") -var scan = require("../scan") o.spec("scan", function() { o("defaults to seed", function() { var parent = stream() - var child = scan(function(out, p) { + var child = stream.scan(function(out, p) { return out - p }, 123, parent) o(child()).equals(123) @@ -15,14 +14,14 @@ o.spec("scan", function() { o("accumulates values as expected", function() { var parent = stream() - var child = scan(function(arr, p) { + var child = stream.scan(function(arr, p) { return arr.concat(p) }, [], parent) parent(7) parent("11") parent(undefined) - parent({ a: 1 }) + parent({a: 1}) var result = child() // deepEquals fails on arrays? @@ -32,4 +31,3 @@ o.spec("scan", function() { o(result[3]).deepEquals({a: 1}) }) }) - diff --git a/stream/tests/test-scanMerge.js b/stream/tests/test-scanMerge.js index 1e396176..cbf1e7f1 100644 --- a/stream/tests/test-scanMerge.js +++ b/stream/tests/test-scanMerge.js @@ -2,14 +2,13 @@ var o = require("../../ospec/ospec") var stream = require("../stream") -var scanMerge = require("../scanMerge") o.spec("scanMerge", function() { o("defaults to seed", function() { var parent1 = stream() var parent2 = stream() - var child = scanMerge([ + var child = stream.scanMerge([ [parent1, function(out, p1) { return out + p1 }], @@ -25,7 +24,7 @@ o.spec("scanMerge", function() { var parent1 = stream() var parent2 = stream() - var child = scanMerge([ + var child = stream.scanMerge([ [parent1, function(out, p1) { return out + p1 }], diff --git a/stream/tests/test-stream.js b/stream/tests/test-stream.js index 92e16620..09b01209 100644 --- a/stream/tests/test-stream.js +++ b/stream/tests/test-stream.js @@ -1,7 +1,6 @@ "use strict" var o = require("../../ospec/ospec") -var callAsync = require("../../test-utils/callAsync") var Stream = require("../stream") o.spec("stream", function() { @@ -105,7 +104,7 @@ o.spec("stream", function() { var streams = [] var a = Stream() var b = Stream() - var c = Stream.combine(function(a, b, changed) { + Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) @@ -119,7 +118,7 @@ o.spec("stream", function() { var streams = [] var a = Stream(3) var b = Stream(5) - var c = Stream.combine(function(a, b, changed) { + Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) @@ -130,7 +129,7 @@ o.spec("stream", function() { }) o("combine can return undefined", function() { var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { return undefined }, [a]) @@ -138,7 +137,7 @@ o.spec("stream", function() { }) o("combine can return stream", function() { var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { return Stream(2) }, [a]) @@ -146,7 +145,7 @@ o.spec("stream", function() { }) o("combine can return pending stream", function() { var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { return Stream() }, [a]) @@ -155,22 +154,22 @@ o.spec("stream", function() { o("combine can halt", function() { var count = 0 var a = Stream(1) - var b = Stream.combine(function(a) { + var b = Stream.combine(function() { return Stream.HALT - }, [a]) - ["fantasy-land/map"](function() { + }, [a])["fantasy-land/map"](function() { count++ return 1 }) o(b()).equals(undefined) + o(count).equals(0) }) o("combine will throw with a helpful error if given non-stream values", function () { var spy = o.spy() var a = Stream(1) var thrown = null; try { - var b = Stream.combine(spy, [a, '']) + Stream.combine(spy, [a, ""]) } catch (e) { thrown = e } @@ -210,8 +209,9 @@ o.spec("stream", function() { var a = Stream() var b = Stream() - var all = Stream.merge([a.map(id), b.map(id)]).map(function(data) { + Stream.merge([a.map(id), b.map(id)]).map(function(data) { value = data[0] + data[1] + return undefined }) a(1) @@ -263,6 +263,18 @@ o.spec("stream", function() { o(doubled()).equals(4) }) + o("end stream can be mapped to", function() { + var stream = Stream() + var spy = o.spy() + + stream.end.map(spy) + + o(spy.callCount).equals(0) + + stream.end(true) + + o(spy.callCount).equals(1) + }) }) o.spec("valueOf", function() { o("works", function() { @@ -344,7 +356,7 @@ o.spec("stream", function() { }) o("works with pending stream", function() { var stream = Stream(undefined) - var mapped = stream["fantasy-land/map"](function(value) {return Stream()}) + var mapped = stream["fantasy-land/map"](function() {return Stream()}) o(mapped()()).equals(undefined) }) diff --git a/test-utils/components.js b/test-utils/components.js new file mode 100644 index 00000000..109af9bb --- /dev/null +++ b/test-utils/components.js @@ -0,0 +1,29 @@ +"use strict" + +module.exports = [ + { + kind: "POJO", + create: function(methods) { + var res = {view: function() {return {tag:"div"}}} + Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) + return res + } + }, { + kind: "constructible", + create: function(methods) { + function res(){} + res.prototype.view = function() {return {tag:"div"}} + Object.keys(methods || {}).forEach(function(m){res.prototype[m] = methods[m]}) + return res + } + }, { + kind: "closure", + create: function(methods) { + return function() { + var res = {view: function() {return {tag:"div"}}} + Object.keys(methods || {}).forEach(function(m){res[m] = methods[m]}) + return res + } + } + } +] diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 5da93b98..a827070a 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -96,7 +96,7 @@ module.exports = function() { declList = declList.replace( /("(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*')|\/\*[\s\S]*?\*\//g, function(m, str){ - return str || '' + return str || "" } ) /*eslint-disable no-cond-assign*/ @@ -115,7 +115,7 @@ module.exports = function() { var activeElement var $window = { document: { - createElement: function(tag, is) { + createElement: function(tag) { var cssText = "" var style = {} Object.defineProperty(style, "cssText", { @@ -211,11 +211,11 @@ module.exports = function() { else this.setAttribute("class", value) }, focus: function() {activeElement = this}, - addEventListener: function(type, callback, useCapture) { + addEventListener: function(type, callback) { if (events[type] == null) events[type] = [callback] else events[type].push(callback) }, - removeEventListener: function(type, callback, useCapture) { + removeEventListener: function(type, callback) { if (events[type] != null) { var index = events[type].indexOf(callback) if (index > -1) events[type].splice(index, 1) @@ -241,7 +241,6 @@ module.exports = function() { } if (element.nodeName === "A") { - var href Object.defineProperty(element, "href", { get: function() {return this.attributes["href"] === undefined ? "" : "[FIXME implement]"}, set: function(value) {this.setAttribute("href", value)}, @@ -271,7 +270,9 @@ module.exports = function() { enumerable: true, }) } - + + /* eslint-disable radix */ + if (element.nodeName === "CANVAS") { Object.defineProperty(element, "width", { get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].nodeValue) || 0) : 300}, @@ -283,6 +284,8 @@ module.exports = function() { }) } + /* eslint-enable radix */ + function getOptions(element) { var options = [] for (var i = 0; i < element.childNodes.length; i++) { @@ -297,17 +300,18 @@ module.exports = function() { element.firstChild != null ? element.firstChild.nodeValue : "" } if (element.nodeName === "SELECT") { - var selectedValue, selectedIndex = 0 + // var selectedValue + var selectedIndex = 0 Object.defineProperty(element, "selectedIndex", { get: function() {return getOptions(this).length > 0 ? selectedIndex : -1}, set: function(value) { var options = getOptions(this) if (value >= 0 && value < options.length) { - selectedValue = getOptionValue(options[selectedIndex]) + // selectedValue = getOptionValue(options[selectedIndex]) selectedIndex = value } else { - selectedValue = "" + // selectedValue = "" selectedIndex = -1 } }, @@ -323,12 +327,12 @@ module.exports = function() { var stringValue = String(value) for (var i = 0; i < options.length; i++) { if (getOptionValue(options[i]) === stringValue) { - selectedValue = stringValue + // selectedValue = stringValue selectedIndex = i return } } - selectedValue = stringValue + // selectedValue = stringValue selectedIndex = -1 }, enumerable: true, diff --git a/test-utils/parseURL.js b/test-utils/parseURL.js index f5e76e8d..e60cc531 100644 --- a/test-utils/parseURL.js +++ b/test-utils/parseURL.js @@ -3,7 +3,7 @@ module.exports = function parseURL(url, root) { var data = {} var protocolIndex = url.indexOf("://") - var pathnameIndex = protocolIndex > - 1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") + var pathnameIndex = protocolIndex > -1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") var searchIndex = url.indexOf("?") var hashIndex = url.indexOf("#") if ((pathnameIndex > searchIndex && searchIndex > -1) || (pathnameIndex > hashIndex && hashIndex > -1)) pathnameIndex = -1 diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js index 91e7a136..ebd963fd 100644 --- a/test-utils/pushStateMock.js +++ b/test-utils/pushStateMock.js @@ -4,7 +4,7 @@ var parseURL = require("../test-utils/parseURL") module.exports = function(options) { if (options == null) options = {} - + var $window = options.window || {} var protocol = options.protocol || "http:" var hostname = options.hostname || "localhost" @@ -33,7 +33,7 @@ module.exports = function(options) { } return isNew } - + function prefix(prefix, value) { if (value === "") return "" return (value.charAt(0) !== prefix ? prefix : "") + value diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html index e24fa2f8..51b04d73 100644 --- a/test-utils/tests/index.html +++ b/test-utils/tests/index.html @@ -14,12 +14,14 @@ + + diff --git a/test-utils/tests/test-browserMock.js b/test-utils/tests/test-browserMock.js index d47c58ce..861f1ef0 100644 --- a/test-utils/tests/test-browserMock.js +++ b/test-utils/tests/test-browserMock.js @@ -17,7 +17,7 @@ o.spec("browserMock", function() { }) o("$window.onhashchange can be reached from the pushStateMock functions", function(done) { $window.onhashchange = o.spy() - $window.location.hash = '#a' + $window.location.hash = "#a" callAsync(function(){ o($window.onhashchange.callCount).equals(1) @@ -33,7 +33,7 @@ o.spec("browserMock", function() { }) o("$window.onunload can be reached from the pushStateMock functions", function() { $window.onunload = o.spy() - $window.location.href = '/a' + $window.location.href = "/a" o($window.onunload.callCount).equals(1) }) diff --git a/test-utils/tests/test-components.js b/test-utils/tests/test-components.js new file mode 100644 index 00000000..e0ede703 --- /dev/null +++ b/test-utils/tests/test-components.js @@ -0,0 +1,54 @@ +"use strict" + +var o = require("../../ospec/ospec") +var components = require("../../test-utils/components") + +o.spec("test-utils/components", function() { + var test = o.spy(function(component) { + return function() { + o("works", function() { + o(typeof component.kind).equals("string") + + var methods = {oninit: function(){}, view: function(){}} + + var cmp1, cmp2 + + if (component.kind === "POJO") { + cmp1 = component.create() + cmp2 = component.create(methods) + } else if (component.kind === "constructible") { + cmp1 = new (component.create()) + cmp2 = new (component.create(methods)) + } else if (component.kind === "closure") { + cmp1 = component.create()() + cmp2 = component.create(methods)() + } else { + throw new Error("unexpected component kind") + } + + o(cmp1 != null).equals(true) + o(typeof cmp1.view).equals("function") + + var vnode = cmp1.view() + + o(vnode != null).equals(true) + o(vnode).deepEquals({tag: "div"}) + + if (component.kind !== "constructible") { + o(cmp2).deepEquals(methods) + } else { + // deepEquals doesn't search the prototype, do it manually + o(cmp2 != null).equals(true) + o(cmp2.view).equals(methods.view) + o(cmp2.oninit).equals(methods.oninit) + } + }) + } + }) + o.after(function(){ + o(test.callCount).equals(3) + }) + components.forEach(function(component) { + o.spec(component.kind, test(component)) + }) +}) diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js index edd896d5..aee68832 100644 --- a/test-utils/tests/test-domMock.js +++ b/test-utils/tests/test-domMock.js @@ -321,7 +321,7 @@ o.spec("domMock", function() { o(div.getAttribute("id")).equals("aaa") }) }) - + o.spec("setAttribute", function() { o("works", function() { var div = $document.createElement("div") @@ -393,7 +393,6 @@ o.spec("domMock", function() { o.spec("textContent", function() { o("works", function() { var div = $document.createElement("div") - var a = $document.createElement("a") div.textContent = "aaa" o(div.childNodes.length).equals(1) @@ -402,7 +401,6 @@ o.spec("domMock", function() { }) o("works with empty string", function() { var div = $document.createElement("div") - var a = $document.createElement("a") div.textContent = "" o(div.childNodes.length).equals(0) @@ -514,8 +512,8 @@ o.spec("domMock", function() { var div = $document.createElement("div") div.style.cssText = "background: url(';'); font-family: \";\"" - o(div.style.background).equals("url(';')") - o(div.style.fontFamily).equals("\";\"") + o(div.style.background).equals("url(';')") + o(div.style.fontFamily).equals('";"') o(div.style.cssText).equals("background: url(';'); font-family: \";\";") }) o("comments in style.cssText are stripped", function(){ @@ -534,9 +532,10 @@ o.spec("domMock", function() { }) o("setting style throws", function () { + var div = $document.createElement("div") var err = false try { - div.style = '' + div.style = "" } catch (e) { err = e } @@ -919,55 +918,55 @@ o.spec("domMock", function() { o.spec("canvas width and height", function() { o("setting property works", function() { var canvas = $document.createElement("canvas") - + canvas.width = 100 o(canvas.attributes["width"].nodeValue).equals("100") o(canvas.width).equals(100) - + canvas.height = 100 o(canvas.attributes["height"].nodeValue).equals("100") o(canvas.height).equals(100) }) o("setting string casts to number", function() { var canvas = $document.createElement("canvas") - + canvas.width = "100" o(canvas.attributes["width"].nodeValue).equals("100") o(canvas.width).equals(100) - + canvas.height = "100" o(canvas.attributes["height"].nodeValue).equals("100") o(canvas.height).equals(100) }) o("setting float casts to int", function() { var canvas = $document.createElement("canvas") - + canvas.width = 1.2 o(canvas.attributes["width"].nodeValue).equals("1") o(canvas.width).equals(1) - + canvas.height = 1.2 o(canvas.attributes["height"].nodeValue).equals("1") o(canvas.height).equals(1) }) o("setting percentage fails", function() { var canvas = $document.createElement("canvas") - + canvas.width = "100%" o(canvas.attributes["width"].nodeValue).equals("0") o(canvas.width).equals(0) - + canvas.height = "100%" o(canvas.attributes["height"].nodeValue).equals("0") o(canvas.height).equals(0) }) o("setting attribute works", function() { var canvas = $document.createElement("canvas") - + canvas.setAttribute("width", "100%") o(canvas.attributes["width"].nodeValue).equals("100%") o(canvas.width).equals(100) - + canvas.setAttribute("height", "100%") o(canvas.attributes["height"].nodeValue).equals("100%") o(canvas.height).equals(100) diff --git a/test-utils/tests/test-pushStateMock.js b/test-utils/tests/test-pushStateMock.js index 3cb51716..41efacab 100644 --- a/test-utils/tests/test-pushStateMock.js +++ b/test-utils/tests/test-pushStateMock.js @@ -168,13 +168,13 @@ o.spec("pushStateMock", function() { }) o.spec("set protocol", function() { o("setting protocol throws", function(done) { - var old = $window.location.href try { $window.location.protocol = "https://" } catch (e) { - done() + return done() } + throw new Error("Expected an error") }) }) o.spec("set port", function() { @@ -413,17 +413,17 @@ o.spec("pushStateMock", function() { }) o("replaceState does not break forward history", function() { $window.onpopstate = o.spy() - + $window.history.pushState(null, null, "b") $window.history.back() o($window.onpopstate.callCount).equals(1) o($window.location.href).equals("http://localhost/") - + $window.history.replaceState(null, null, "a") - + o($window.location.href).equals("http://localhost/a") - + $window.history.forward() o($window.onpopstate.callCount).equals(2) @@ -431,46 +431,46 @@ o.spec("pushStateMock", function() { }) o("pushstate retains state", function() { $window.onpopstate = o.spy() - + $window.history.pushState({a: 1}, null, "#a") $window.history.pushState({b: 2}, null, "#b") - + o($window.onpopstate.callCount).equals(0) $window.history.back() - + o($window.onpopstate.callCount).equals(1) o($window.onpopstate.args[0].type).equals("popstate") o($window.onpopstate.args[0].state).deepEquals({a: 1}) $window.history.back() - + o($window.onpopstate.callCount).equals(2) o($window.onpopstate.args[0].type).equals("popstate") o($window.onpopstate.args[0].state).equals(null) $window.history.forward() - + o($window.onpopstate.callCount).equals(3) o($window.onpopstate.args[0].type).equals("popstate") o($window.onpopstate.args[0].state).deepEquals({a: 1}) $window.history.forward() - + o($window.onpopstate.callCount).equals(4) o($window.onpopstate.args[0].type).equals("popstate") o($window.onpopstate.args[0].state).deepEquals({b: 2}) }) o("replacestate replaces state", function() { $window.onpopstate = o.spy(pop) - + $window.history.replaceState({a: 1}, null, "a") - + o($window.history.state).deepEquals({a: 1}) - + $window.history.pushState(null, null, "a") $window.history.back() - + function pop(e) { o(e.state).deepEquals({a: 1}) o($window.history.state).deepEquals({a: 1}) diff --git a/test-utils/tests/test-xhrMock.js b/test-utils/tests/test-xhrMock.js index cef2d9ca..8bdfcf21 100644 --- a/test-utils/tests/test-xhrMock.js +++ b/test-utils/tests/test-xhrMock.js @@ -5,13 +5,13 @@ var xhrMock = require("../../test-utils/xhrMock") var parseQueryString = require("../../querystring/parse") o.spec("xhrMock", function() { - var $window, ajax + var $window o.beforeEach(function() { $window = xhrMock() }) o.spec("xhr", function() { - o("works", function(done, timeout) { + o("works", function(done) { $window.$defineRoutes({ "GET /item": function(request) { o(request.url).equals("/item") @@ -29,7 +29,7 @@ o.spec("xhrMock", function() { } xhr.send() }) - o("works w/ search", function(done, timeout) { + o("works w/ search", function(done) { $window.$defineRoutes({ "GET /item": function(request) { o(request.query).equals("?a=b") @@ -45,7 +45,7 @@ o.spec("xhrMock", function() { } xhr.send() }) - o("works w/ body", function(done, timeout) { + o("works w/ body", function(done) { $window.$defineRoutes({ "POST /item": function(request) { o(request.body).equals("a=b") @@ -61,7 +61,7 @@ o.spec("xhrMock", function() { } xhr.send("a=b") }) - o("handles routing error", function(done, timeout) { + o("handles routing error", function(done) { var xhr = new $window.XMLHttpRequest() xhr.open("GET", "/nonexistent") xhr.onreadystatechange = function() { @@ -113,7 +113,7 @@ o.spec("xhrMock", function() { done() } }) - o("works with other querystring params", function(done, timeout) { + o("works with other querystring params", function(done) { $window.$defineRoutes({ "GET /test": function(request) { var queryData = parseQueryString(request.query) diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index 57c2887f..d3bbf5b8 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -6,7 +6,7 @@ var parseQueryString = require("../querystring/parse") module.exports = function() { var routes = {} - var callback = "callback" + // var callback = "callback" var serverErrorHandler = function(url) { return {status: 500, responseText: "server error, most likely the URL was not defined " + url} } @@ -43,7 +43,6 @@ module.exports = function() { } self.readyState = 4 if (args.async === true) { - var s = new Date callAsync(function() { if (typeof self.onreadystatechange === "function") self.onreadystatechange() }) @@ -64,7 +63,7 @@ module.exports = function() { var urlData = parseURL(element.src, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) var handler = routes["GET " + urlData.pathname] || serverErrorHandler.bind(null, element.src) var data = handler({url: urlData.pathname, query: urlData.search, body: null}) - var query = parseQueryString(urlData.search) + parseQueryString(urlData.search) callAsync(function() { if (data.status === 200) { new Function("$window", "with ($window) return " + data.responseText).call($window, $window) @@ -83,8 +82,8 @@ module.exports = function() { $defineRoutes: function(rules) { routes = rules }, - $defineJSONPCallbackKey: function(key) { - callback = key + $defineJSONPCallbackKey: function(/* key */) { + // callback = key }, } return $window diff --git a/tests/test-api.js b/tests/test-api.js index 0a5824f9..3240141c 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -2,6 +2,7 @@ var o = require("../ospec/ospec") var browserMock = require("../test-utils/browserMock") +var components = require("../test-utils/components") o.spec("api", function() { var m @@ -9,13 +10,13 @@ o.spec("api", function() { o.beforeEach(function() { var mock = browserMock() if (typeof global !== "undefined") global.window = mock - m = require("../mithril") + m = require("../mithril") // eslint-disable-line global-require }) - + o.spec("m", function() { o("works", function() { var vnode = m("div") - + o(vnode.tag).equals("div") }) }) @@ -29,7 +30,7 @@ o.spec("api", function() { o.spec("m.trust", function() { o("works", function() { var vnode = m.trust("
") - + o(vnode.tag).equals("<") o(vnode.children).equals("
") }) @@ -37,7 +38,7 @@ o.spec("api", function() { o.spec("m.fragment", function() { o("works", function() { var vnode = m.fragment({key: 123}, [m("div")]) - + o(vnode.tag).equals("[") o(vnode.key).equals(123) o(vnode.children.length).equals(1) @@ -48,115 +49,26 @@ o.spec("api", function() { o("works", function() { var spy = o.spy() var handler = m.withAttr("value", spy) - + handler({currentTarget: {value: 10}}) - + o(spy.args[0]).equals(10) }) }) o.spec("m.parseQueryString", function() { o("works", function() { var query = m.parseQueryString("?a=1&b=2") - + o(query).deepEquals({a: "1", b: "2"}) }) }) o.spec("m.buildQueryString", function() { o("works", function() { var query = m.buildQueryString({a: 1, b: 2}) - + o(query).equals("a=1&b=2") }) }) - o.spec("m.render", function() { - o("works", function() { - var root = window.document.createElement("div") - m.render(root, m("div")) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.mount", function() { - o("works", function() { - var root = window.document.createElement("div") - m.mount(root, {view: function() {return m("div")}}) - - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - }) - }) - o.spec("m.route", function() { - o("works", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.prefix", function(done) { - var root = window.document.createElement("div") - m.route.prefix("#") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(root.childNodes.length).equals(1) - o(root.firstChild.nodeName).equals("DIV") - - done() - }, FRAME_BUDGET) - }) - o("m.route.get", function(done) { - var root = window.document.createElement("div") - m.route(root, "/a", { - "/a": {view: function() {return m("div")}} - }) - - setTimeout(function() { - o(m.route.get()).equals("/a") - - done() - }, FRAME_BUDGET) - }) - o("m.route.set", function(done, timeout) { - timeout(100) - var root = window.document.createElement("div") - m.route(root, "/a", { - "/:id": {view: function() {return m("div")}} - }) - - setTimeout(function() { - m.route.set("/b") - setTimeout(function() { - o(m.route.get()).equals("/b") - - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) - }) - }) - o.spec("m.redraw", function() { - o("works", function(done) { - var count = 0 - var root = window.document.createElement("div") - m.mount(root, {view: function() {count++}}) - setTimeout(function() { - m.redraw() - - o(count).equals(2) - - done() - }, FRAME_BUDGET) - }) - }) o.spec("m.request", function() { o("works", function() { o(typeof m.request).equals("function") // TODO improve @@ -167,4 +79,99 @@ o.spec("api", function() { o(typeof m.jsonp).equals("function") // TODO improve }) }) -}) \ No newline at end of file + o.spec("m.render", function() { + o("works", function() { + var root = window.document.createElement("div") + m.render(root, m("div")) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + components.forEach(function(cmp){ + o.spec(cmp.kind, function(){ + var createComponent = cmp.create + + o.spec("m.mount", function() { + o("works", function() { + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {return m("div")}})) + + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + }) + }) + o.spec("m.route", function() { + o("works", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.prefix", function(done) { + var root = window.document.createElement("div") + m.route.prefix("#") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(root.childNodes.length).equals(1) + o(root.firstChild.nodeName).equals("DIV") + + done() + }, FRAME_BUDGET) + }) + o("m.route.get", function(done) { + var root = window.document.createElement("div") + m.route(root, "/a", { + "/a": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + o(m.route.get()).equals("/a") + + done() + }, FRAME_BUDGET) + }) + o("m.route.set", function(done, timeout) { + timeout(100) + var root = window.document.createElement("div") + m.route(root, "/a", { + "/:id": createComponent({view: function() {return m("div")}}) + }) + + setTimeout(function() { + m.route.set("/b") + setTimeout(function() { + o(m.route.get()).equals("/b") + + done() + }, FRAME_BUDGET) + }, FRAME_BUDGET) + }) + }) + o.spec("m.redraw", function() { + o("works", function(done) { + var count = 0 + var root = window.document.createElement("div") + m.mount(root, createComponent({view: function() {count++}})) + setTimeout(function() { + m.redraw() + + o(count).equals(2) + + done() + }, FRAME_BUDGET) + }) + }) + }) + }) +})