Merge branch 'next'

This commit is contained in:
Isiah Meadows 2018-12-06 00:34:15 -05:00
commit aacfda739a
81 changed files with 6700 additions and 2285 deletions

View file

@ -3,5 +3,8 @@ coverage/
docs/lib/
examples/
/mithril.js
/mithril.mjs
/mithril.min.js
/mithril.min.mjs
/stream/stream.mjs
node_modules/

2
.gitattributes vendored
View file

@ -1,5 +1,7 @@
* text=auto
/mithril.js binary
/mithril.min.js binary
/mithril.mjs binary
/mithril.min.mjs binary
/package-lock.json binary
/yarn.lock binary

4
.github/CODEOWNERS vendored
View file

@ -1 +1,5 @@
render/ @pygy
render/ @isiahmeadows
api/ @isiahmeadows
performance/ @isiahmeadows
util/ @isiahmeadows

View file

@ -1,3 +1,13 @@
---
name: "\U0001F41BBug report"
about: Report a bug in MithrilJS
---
### Mithril Version: x.x.x
<!--- Provide the exact version of MithrilJS in which you see the bug. -->
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
@ -12,7 +22,7 @@
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
@ -24,9 +34,12 @@
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
<details>
<summary>Additional Information</summary>
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Browser Name and version:
* Operating System and version (desktop or mobile):
* Link to your project:
</details>

View file

@ -0,0 +1,25 @@
---
name: "\U0001F680Feature or Enhancement"
about: Suggest an Idea or Enhancement for MithrilJS
---
### Description
<!--- Provide a detailed description of the change or addition you are proposing -->
### Why
<!--- Why is this change important to you? How would you use it? -->
<!--- How can it benefit other users? -->
### Possible Implementation & Open Questions
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
<!--- What still needs to be discussed -->
### Is this something you're interested in working on?
<!--- Yes or no -->

View file

@ -21,6 +21,8 @@ before_script:
- npm run build-browser
# Pass -save so it'll update the readme as well
- npm run build-min -- -save
# must run after build-min in order to generate min.mjs
- npm run build-esm
# Run tests, lint, and then check for perf regressions
script:

View file

@ -18,11 +18,11 @@ mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://ww
## What is Mithril?
A modern client-side Javascript framework for building Single Page Applications. It's small (<!-- size -->8.89 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
A modern client-side Javascript framework for building Single Page Applications. It's small (<!-- size -->8.86 KB<!-- /size --> gzipped), fast and provides routing and XHR utilities out of the box.
Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
Browsers all the way back to IE9 are supported, no polyfills required 👌.
Mithril supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌
## Installation
@ -30,12 +30,13 @@ Browsers all the way back to IE9 are supported, no polyfills required 👌.
```html
<script src="https://unpkg.com/mithril"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/mithril/mithril.js"></script>
```
### npm
```bash
$ npm install mithril
$ npm install mithril --save
```
The ["Getting started" guide](https://mithril.js.org/#getting-started) is a good place to start learning how to use mithril.

View file

@ -3,18 +3,13 @@
var coreRenderer = require("../render/render")
function throttle(callback) {
//60fps translates to 16.6ms, round it down since setTimeout requires int
var delay = 16
var last = 0, pending = null
var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
var pending = null
return function() {
var elapsed = Date.now() - last
if (pending === null) {
pending = timeout(function() {
pending = requestAnimationFrame(function() {
pending = null
callback()
last = Date.now()
}, delay - elapsed)
})
}
}
}
@ -22,11 +17,6 @@ function throttle(callback) {
module.exports = function($window, throttleMock) {
var renderService = coreRenderer($window)
renderService.setEventCallback(function(e) {
if (e.redraw === false) e.redraw = undefined
else redraw()
})
var callbacks = []
var rendering = false
@ -47,5 +37,6 @@ module.exports = function($window, throttleMock) {
var redraw = (throttleMock || throttle)(sync)
redraw.sync = sync
renderService.setRedraw(redraw)
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}

View file

@ -5,6 +5,19 @@ var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw")
// Because Node doesn't have this.
if (typeof requestAnimationFrame !== "function") {
global.requestAnimationFrame = (function (delay, last) {
return function(callback) {
var elapsed = Date.now() - last
return setTimeout(function() {
callback()
last = Date.now()
}, delay - elapsed)
}
})(16, 0)
}
o.spec("redrawService", function() {
var root, redrawService, $document
o.beforeEach(function() {

View file

@ -16,7 +16,7 @@ function parse(file) {
}
var error
function run(input, output) {
module.exports = function (input) {
var modules = {}
var bindings = {}
var declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm
@ -115,19 +115,7 @@ function run(input, output) {
.replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
code = ";(function() {\n" + code + "\n}());"
if (!isFile(output) || code !== read(output)) {
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
error = null
fs.writeFileSync(output, code, "utf8")
}
}
module.exports = function(input, output, options) {
run(input, output)
if (options && options.watch) {
fs.watch(process.cwd(), {recursive: true}, function(file) {
if (typeof file === "string" && path.resolve(output) !== path.resolve(file)) run(input, output)
})
}
//try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
error = null
return code
}

View file

@ -1,10 +1,11 @@
"use strict"
var fs = require("fs");
var fs = require("fs")
var zlib = require("zlib")
var chokidar = require("chokidar")
var Terser = require("terser")
var bundle = require("./bundle")
var minify = require("./minify")
var aliases = {o: "output", m: "minify", w: "watch", s: "save"}
var params = {}
@ -29,14 +30,20 @@ function format(n) {
return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
}
bundle(params.input, params.output, {watch: params.watch})
if (params.minify) {
// mFiles = { original: String(mithril.js), compressed: String(mithril.min.js) }
var mFiles = minify(params.output, {watch: params.watch})
var originalSize = mFiles.original.length
var compressedSize = mFiles.compressed.length
var originalGzipSize = zlib.gzipSync(mFiles.original).byteLength
var compressedGzipSize = zlib.gzipSync(mFiles.compressed).byteLength
function build() {
var original = bundle(params.input)
if (!params.minify) {
fs.writeFileSync(params.output, original, "utf-8")
return
}
console.log("minifying...")
var minified = Terser.minify(original)
if (minified.error) throw new Error(minified.error)
fs.writeFileSync(params.output, minified.code, "utf-8")
var originalSize = original.length
var compressedSize = minified.code.length
var originalGzipSize = zlib.gzipSync(original).byteLength
var compressedGzipSize = zlib.gzipSync(minified.code).byteLength
console.log("Original size: " + format(originalGzipSize) + " bytes gzipped (" + format(originalSize) + " bytes uncompressed)")
console.log("Compiled size: " + format(compressedGzipSize) + " bytes gzipped (" + format(compressedSize) + " bytes uncompressed)")
@ -52,4 +59,7 @@ if (params.minify) {
)
)
}
}
}
build()
if (params.watch) chokidar.watch(".", {ignored: params.output}).on("all", build)

View file

@ -1,26 +0,0 @@
"use strict"
var fs = require("fs")
var UglifyES = require("uglify-es")
module.exports = function(filePath, options) {
function minify(filePath) {
var original = fs.readFileSync(filePath, "utf8"),
uglified = UglifyES.minify(original),
compressed = uglified.code
if (uglified.error) throw new Error(uglified.error)
fs.writeFileSync(filePath, compressed, "utf8")
return {original: original, compressed: compressed}
}
function run() {
console.log("minifying...")
return minify(filePath)
}
if (options && options.watch) fs.watchFile(filePath, run)
return run()
}

View file

@ -6,9 +6,6 @@ var bundle = require("../bundle")
var fs = require("fs")
var ns = "./"
function read(filepath) {
try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {/* ignore */}
}
function write(filepath, data) {
try {var exists = fs.statSync(ns + filepath).isFile()} catch (e) {/* ignore */}
if (exists) throw new Error("Don't call `write('" + filepath + "')`. Cannot overwrite file")
@ -22,266 +19,221 @@ o.spec("bundler", function() {
o("relative imports works", function() {
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(";(function() {\nvar b = 1\n}());")
o(bundle(ns + "a.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;")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b = 1;\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nlet b = 1\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nconst b = 1\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar a = {}\na.b = 1\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b = {}\nb = 1\n}());")
o(bundle(ns + "a.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')
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(';(function() {\n"use strict"\nvar b = 1\n}());')
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\n'use strict'\nvar b = 1\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(';(function() {\n"use strict"\nvar b = 1\n}());')
o(bundle(ns + "a.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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nwindow.a = 1\nvar b = function() {return a}\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\n1 + 1\n}());")
o(bundle(ns + "a.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 = []")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\n}());")
o(bundle(ns + "a.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 = []")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0\n\t.toString()\n}());")
o(bundle(ns + "a.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() {}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = function() {}\nvar b = _0()\n}());")
o(bundle(ns + "a.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() {}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = function() {}\nvar b = _0\n()\n}());")
o(bundle(ns + "a.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}')
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = []\nvar b = _0.toString()\nvar b0 = _0\nvar c = function() {return b0}\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b = 1\nvar x\nvar c = 2\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar x = {}\nvar bb = 1\nx.b = bb\nvar cc = 2\nx.c = cc\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(';(function() {\nvar x = {}\nvar bb = 1\nx["b"] = bb\nvar cc = 2\nx["c"] = cc\n}());')
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b0 = 1\nvar b = 2\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b = {}\nb.x = 1\n}());")
o(bundle(ns + "a.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")
bundle(ns + "a.js", ns + "out.js")
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}());")
o(bundle(ns + "a.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")
remove("d.js")
remove("out.js")
})
o("works if included multiple times", function() {
write("a.js", "module.exports = 123")
write("b.js", 'var a = require("./a").toString()\nmodule.exports = a')
write("c.js", 'var a = require("./a").toString()\nvar b = require("./b")')
bundle(ns + "c.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}());")
o(bundle(ns + "c.js")).equals(";(function() {\nvar _0 = 123\nvar a = _0.toString()\nvar a0 = _0.toString()\nvar b = a0\n}());")
remove("a.js")
remove("b.js")
@ -291,9 +243,8 @@ o.spec("bundler", function() {
write("a.js", "module.exports = 123")
write("b.js", 'var a = require("./a").toString()\nmodule.exports = a')
write("c.js", 'var b = require("./b")\nvar a = require("./a").toString()')
bundle(ns + "c.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}());")
o(bundle(ns + "c.js")).equals(";(function() {\nvar _0 = 123\nvar a0 = _0.toString()\nvar b = a0\nvar a = _0.toString()\n}());")
remove("a.js")
remove("b.js")
@ -304,82 +255,68 @@ o.spec("bundler", function() {
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(";(function() {\nvar d = 1\nvar b = function() {return d + 1}\nvar c = function() {return d + 2}\n}());")
o(bundle(ns + "a.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")
remove("d.js")
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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b0 = 1\nvar b = function() {return b0}\n}());")
o(bundle(ns + "a.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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar a = 1\nvar a0 = 2\nvar b = function() {return a0}\n}());")
o(bundle(ns + "a.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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nfunction a() {}\nvar a0 = 2\nvar b = function() {return a0}\n}());")
o(bundle(ns + "a.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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar a = 1\nvar b = function() {return a}\nvar a0 = 2\nvar c = function() {return a0}\n}());")
o(bundle(ns + "a.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}')
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(';(function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}());')
o(bundle(ns + "a.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}")
bundle(ns + "a.js", ns + "out.js")
o(read("out.js")).equals(";(function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}());")
o(bundle(ns + "a.js")).equals(";(function() {\nvar b0 = {b: 1}\nvar b = function() {return b0.b}\n}());")
remove("a.js")
remove("b.js")
remove("out.js")
})
})

View file

@ -121,48 +121,6 @@ var querystring = m.buildQueryString({a: "1", b: "2"})
---
#### m.withAttr(attrName, callback) - [docs](withAttr.md)
```javascript
var state = {
value: "",
setValue: function(v) {state.value = v}
}
var Component = {
view: function() {
return m("input", {
oninput: m.withAttr("value", state.setValue),
value: state.value,
})
}
}
m.mount(document.body, Component)
```
---
#### m.prop(initial) - [docs](prop.md)
```javascript
var Component = {
oninit: function(vnode) {
vnode.state.current = m.prop("")
},
view: function(vnode) {
return m("input", {
oninput: function(ev) { vnode.state.current.set(ev.target.value) },
value: vnode.state.current.get(),
})
}
}
m.mount(document.body, Component)
```
---
#### m.trust(htmlString) - [docs](trust.md)
```javascript

View file

@ -96,13 +96,17 @@ Mithril also does not redraw after lifecycle methods. Parts of the UI may be red
If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw.
```javascript
var StableComponent = {
oncreate: function(vnode) {
vnode.state.height = vnode.dom.offsetHeight
m.redraw()
},
view: function() {
return m("div", "This component is " + vnode.state.height + "px tall")
function StableComponent() {
var height = 0
return {
oncreate: function(vnode) {
height = vnode.dom.offsetHeight
m.redraw()
},
view: function() {
return m("div", "This component is " + height + "px tall")
}
}
}
```

View file

@ -25,14 +25,19 @@
- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook.
- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses.
- hyperscript: when attributes have a `null` or `undefined` value, they are treated as if they were absent. [#1773](https://github.com/MithrilJS/mithril.js/issues/1773) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- API: `m.request` errors no longer copy response fields to the error, but instead assign the parsed JSON response to `error.response` and the HTTP status code `error.code`.
- hyperscript: when an attribute is defined on both the first and second argument (as a CSS selector and an `attrs` field, respectively), the latter takes precedence, except for `class` attributes that are still added together. [#2172](https://github.com/MithrilJS/mithril.js/issues/2172) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
- stream: when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200))
- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
- render: Align custom elements to work like normal elements, minus all the HTML-specific magic. ([#2221](https://github.com/MithrilJS/mithril.js/pull/2221))
- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
- cast className using toString ([#2309](https://github.com/MithrilJS/mithril.js/pull/2309))
- render: call attrs' hooks first, with express exception of `onbeforeupdate` to allow attrs to block components from even diffing ([#2297](https://github.com/MithrilJS/mithril.js/pull/2297))
- API: `m.withAttr` removed. ([#2317](https://github.com/MithrilJS/mithril.js/pull/2317))
#### News
- Mithril now only officially supports IE11, Firefox ESR, and the last two versions of Chrome/FF/Edge/Safari. ([#2296](https://github.com/MithrilJS/mithril.js/pull/2296))
- API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
- API: Event handlers may also be objects with `handleEvent` methods ([#1949](https://github.com/MithrilJS/mithril.js/pull/1949), [#2222](https://github.com/MithrilJS/mithril.js/pull/2222)).
- API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930))
@ -43,7 +48,13 @@
- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
- API: Introduction of `m.prop()` ([#2268](https://github.com/MithrilJS/mithril.js/pull/2268))
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
- stream: Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` [#1944](https://github.com/MithrilJS/mithril.js/issues/1944)
- API: ES module bundles are now available for `mithril` and `mithril/stream` ([#2194](https://github.com/MithrilJS/mithril.js/pull/2194) [@porsager](https://github.com/porsager)).
- All of the `m.*` properties from `mithril` are re-exported as named exports in addition to being attached to `m`.
- `m()` itself from `mithril` is exported as the default export.
- `mithril/stream`'s primary export is exported as the default export.
- fragments: allow same attrs/children overloading logic as hyperscript ([#2328](https://github.com/MithrilJS/mithril.js/pull/2328))
#### Bug fixes
@ -59,18 +70,30 @@
- render/events: `Object.prototype` properties can no longer interfere with event listener calls.
- render/events: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed.
- render/hooks: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992)
- docs: tweaks: ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb), [#2205](https://github.com/MithrilJS/mithril.js/pull/2205), [@cavemansspa](https://github.com/cavemansspa), [#2265](https://github.com/MithrilJS/mithril.js/pull/2265), [@isiahmeadows](https://github.com/isiahmeadows))
- docs: tweaks: ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb), [#2205](https://github.com/MithrilJS/mithril.js/pull/2205), [@cavemansspa](https://github.com/cavemansspa), [#2250](https://github.com/MithrilJS/mithril.js/pull/2250) [@isiahmeadows](https://github.com/isiahmeadows), [#2265](https://github.com/MithrilJS/mithril.js/pull/2265), [@isiahmeadows](https://github.com/isiahmeadows))
- render/core: avoid touching `Object.prototype.__proto__` setter with `key: "__proto__"` in certain situations ([#2251](https://github.com/MithrilJS/mithril.js/pull/2251))
- render/core: Vnodes stored in the dom node supplied to `m.render()` are now normalized [#2266](https://github.com/MithrilJS/mithril.js/pull/2266)
- render/core: CSS vars can now be specified in `{style}` attributes ([#2192](https://github.com/MithrilJS/mithril.js/pull/2192) [@barneycarroll](https://github.com/barneycarroll)), ([#2311](https://github.com/MithrilJS/mithril.js/pull/2311) [@porsager](https://github.com/porsager)), ([#2312](https://github.com/MithrilJS/mithril.js/pull/2312) [@isiahmeadows](https://github.com/isiahmeadows))
- request: don't modify params, call `extract`/`serialize`/`deserialize` with correct `this` value ([#2288](https://github.com/MithrilJS/mithril.js/pull/2288))
---
### v1.1.7
- Stream references no longer magically coerce to their underlying values ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150), breaking change: `mithril-stream@2.0.0`)
### v1.2.0
#### News
- Promise polyfill implementation separated from polyfilling logic.
- `PromisePolyfill` is now available on the exported/global `m`.
#### Bug fixes
- core: Workaround for [Internet Explorer bug](https://www.tjvantoll.com/2013/08/30/bugs-with-document-activeelement-in-internet-explorer/) when running in an iframe
#### Note
- Stream references no longer magically coerce to their underlying values ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150), stream breaking change: `mithril-stream@2.0.0`)
---
### v1.1.6
@ -112,6 +135,10 @@
- Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN))
#### Ospec improvements:
- Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager))
---
### v1.1.3

View file

@ -2,23 +2,29 @@
- [Structure](#structure)
- [Lifecycle methods](#lifecycle-methods)
- [Syntactic variants](#syntactic-variants)
- [Passing data to components](#passing-data-to-components)
- [State](#state)
- [Closure component state](#closure-component-state)
- [POJO component state](#pojo-component-state)
- [ES6 Classes](#es6-classes)
- [Class component state](#class-component-state)
- [Avoid anti-patterns](#avoid-anti-patterns)
### Structure
Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.
Any Javascript object that has a view method is a Mithril component. Components can be consumed via the [`m()`](hyperscript.md) utility:
Any Javascript object that has a `view` method is a Mithril component. Components can be consumed via the [`m()`](hyperscript.md) utility:
```javascript
// define your component
var Example = {
view: function() {
view: function(vnode) {
return m("div", "Hello")
}
}
// consume your component
m(Example)
// equivalent HTML
@ -27,31 +33,9 @@ m(Example)
---
### Passing data to components
Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function:
```javascript
m(Example, {name: "Floyd"})
```
This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`:
```javascript
var Example = {
view: function (vnode) {
return m("div", "Hello, " + vnode.attrs.name)
}
}
```
NOTE: Lifecycle methods can also be provided via the `attrs` object, so you should avoid using the lifecycle method names for your own callbacks as they would also be invoked by Mithril. Use lifecycle methods in `attrs` only when you specifically wish to create lifecycle hooks.
---
### Lifecycle methods
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove` and `onbeforeupdate`.
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes. Note that `vnode` is passed as an argument to each lifecycle method, as well as to `view` (with the _previous_ vnode passed additionally to `onbeforeupdate`):
```javascript
var ComponentWithHooks = {
@ -61,7 +45,7 @@ var ComponentWithHooks = {
oncreate: function(vnode) {
console.log("DOM created")
},
onbeforeupdate: function(vnode, old) {
onbeforeupdate: function(newVnode, oldVnode) {
return true
},
onupdate: function(vnode) {
@ -86,7 +70,7 @@ var ComponentWithHooks = {
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
```javascript
function initialize() {
function initialize(vnode) {
console.log("initialized as vnode")
}
@ -101,104 +85,25 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl
---
### Syntactic variants
### Passing data to components
#### ES6 classes
Components can also be written using ES6 class syntax:
Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function:
```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`)
m(Example, {name: "Floyd"})
```
This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`:
```javascript
var Example = {
view: function (vnode) {
return m("div", "Hello, " + vnode.attrs.name)
}
}
```
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, closureComponent)
// 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...
NOTE: Lifecycle methods can also be defined in the `attrs` object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril itself. Use them in `attrs` only when you specifically wish to use them as lifecycle methods.
---
@ -206,13 +111,87 @@ Components can be freely mixed. A Class component can have closure or POJO compo
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.
The state of a component can be accessed three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
Note that unlike many other frameworks, mutating component state does *not* trigger [redraws](autoredraw.md) or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by [m.request](request.md) complete or when the browser navigates to different routes. Mithril's component state mechanisms simply exist as a convenience for applications.
If a state change occurs that is not as a result of any of the above conditions (e.g. after a `setTimeout`), then you can use `m.redraw()` to trigger a redraw manually.
#### Closure Component State
In the above examples, each component is defined as a POJO (Plain Old Javascript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope.
With a closure component, state can simply be maintained by variables that are declared within the outer function:
```javascript
function ComponentWithState(initialVnode) {
// Component state variable, unique to each instance
var count = 0
// POJO component instance: any object with a
// view function which returns a vnode
return {
oninit: function(vnode){
console.log("init a closure component")
},
view: function(vnode) {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: function() {
count += 1
}
}, "Increment count")
)
}
}
}
```
Any functions declared within the closure also have access to its state variables.
```javascript
function ComponentWithState(initialVnode) {
var count = 0
function increment() {
count += 1
}
function decrement() {
count -= 1
}
return {
view: function(vnode) {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: increment
}, "Increment"),
m("button", {
onclick: decrement
}, "Decrement")
)
}
}
}
```
Closure components are consumed in the same way as POJOs, e.g. `m(ComponentWithState, { passedData: ... })`.
A big advantage of closure components is that we don't need to worry about binding `this` when attaching event handler callbacks. In fact `this` is never used at all and we never have to think about `this` context ambiguities.
---
#### POJO Component State
It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
#### At 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.
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 "blueprint" state initialization.
In the example below, `data` is a property of the `ComponentWithInitialState` component's state object.
In the example below, `data` becomes a property of the `ComponentWithInitialState` component's `vnode.state` object.
```javascript
var ComponentWithInitialState = {
@ -228,13 +207,9 @@ m(ComponentWithInitialState)
// <div>Initial content</div>
```
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.
As you can see, 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.
```javascript
var ComponentWithDynamicState = {
@ -274,6 +249,90 @@ m(ComponentUsingThis, {text: "Hello"})
Be aware that when using ES5 functions, the value of `this` in nested anonymous functions is not the component instance. There are two recommended ways to get around this Javascript limitation, use ES6 arrow functions, or if ES6 is not available, use `vnode.state`.
---
### ES6 classes
If it suits your needs (like in object-oriented projects), components can also be written using ES6 class syntax:
```javascript
class ES6ClassComponent {
constructor(vnode) {
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)
])
}
}
```
#### Class Component State
With classes, state can be managed by class instance properties and methods, and accessed via `this`:
```javascript
class ComponentWithState {
constructor(vnode) {
this.count = 0
}
increment() {
this.count += 1
}
decrement() {
this.count -= 1
}
view() {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: () => {this.increment()}
}, "Increment"),
m("button", {
onclick: () => {this.decrement()}
}, "Decrement")
)
}
}
```
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
---
### Mixing component kinds
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
---
### Avoid anti-patterns
@ -306,8 +365,14 @@ var Login = {
login: function() {/*...*/},
view: function() {
return m(".login", [
m("input[type=text]", {oninput: m.withAttr("value", this.setUsername.bind(this)), value: this.username}),
m("input[type=password]", {oninput: m.withAttr("value", this.setPassword.bind(this)), value: this.password}),
m("input[type=text]", {
oninput: function (e) { this.setUsername(e.target.value) },
value: this.username,
}),
m("input[type=password]", {
oninput: function (e) { this.setPassword(e.target.value) },
value: this.password,
}),
m("button", {disabled: !this.canSubmit(), onclick: this.login}, "Login"),
])
}
@ -351,9 +416,18 @@ var Auth = require("../models/Auth")
var Login = {
view: function() {
return m(".login", [
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
m("button", {disabled: !Auth.canSubmit(), onclick: Auth.login}, "Login"),
m("input[type=text]", {
oninput: function (e) { Auth.setUsername(e.target.value) },
value: Auth.username
}),
m("input[type=password]", {
oninput: function (e) { Auth.setPassword(e.target.value) },
value: Auth.password
}),
m("button", {
disabled: !Auth.canSubmit(),
onclick: Auth.login
}, "Login")
])
}
}
@ -363,6 +437,70 @@ This way, the `Auth` module is now the source of truth for auth-related state, a
As a bonus, notice that we no longer need to use `.bind` to keep a reference to the state for the component's event handlers.
#### Don't forward `vnode.attrs` itself to other vnodes
Sometimes, you might want to keep an interface flexible and your implementation simpler by forwarding attributes to a particular child component or element, in this case [Bootstrap's modal](https://getbootstrap.com/docs/4.1/components/modal/). It might be tempting to forward a vnode's attributes like this:
```javascript
// AVOID
var Modal = {
// ...
view: function(vnode) {
return m(".modal[tabindex=-1][role=dialog]", vnode.attrs, [
// forwarding `vnode.attrs` here ^
// ...
])
}
}
```
If you do it like above, you could run into issues when using it:
```js
var MyModal = {
view: function() {
return m(Modal, {
// This toggles it twice, so it doesn't show
onupdate: function(vnode) {
if (toggle) $(vnode.dom).modal("toggle")
}
}, [
// ...
])
}
}
```
Instead, you should forward *single* attributes into vnodes:
```js
// PREFER
var Modal = {
// ...
view: function(vnode) {
return m(".modal[tabindex=-1][role=dialog]", vnode.attrs.attrs, [
// forwarding `attrs:` here ^
// ...
])
}
}
// Example
var MyModal = {
view: function() {
return m(Modal, {
attrs: {
// This toggles it once
onupdate: function(vnode) {
if (toggle) $(vnode.dom).modal("toggle")
}
},
// ...
})
}
}
```
#### Don't manipulate `children`
If a component is opinionated in how it applies attributes or children, you should switch to using custom attributes.

View file

@ -79,17 +79,15 @@ This simplifies the workflow for bug fixes, which means they can be fixed faster
## Why doesn't the Mithril codebase use ES6 via Babel? Would a PR to upgrade be welcome?
## Why doesn't the Mithril codebase use ES6 via Babel or Bublé? Would a PR to upgrade be welcome?
Being able to run Mithril raw source code in IE is a requirement for all browser-related modules in this repo.
In addition, ES6 features are usually less performant than equivalent ES5 code, and transpiled code is bulkier.
Being able to run Mithril's raw source code in all supported browsers is a requirement for all browser-related modules in this repo. In addition, transpiled code is generally much bulkier.
## Why doesn't the Mithril codebase use trailing semi-colons? Would a PR to add them be welcome?
I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent.
I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent. Besides, [we aren't the only one who've decided to drop the semicolon](https://standardjs.com/#who-uses-javascript-standard-style). (We don't use Standard, though.)

View file

@ -37,9 +37,9 @@ Generates a fragment [vnode](vnodes.md)
Argument | Type | Required | Description
----------- | --------------------------------------------------- | -------- | ---
`attrs` | `Object` | Yes | A map of attributes
`children` | `Array<Vnode|String|Number|Boolean|null|undefined>` | Yes | A list of vnodes
**returns** | `Vnode` | | A fragment [vnode](vnodes.md)
`attrs` | `Object` | No | HTML attributes or element properties
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats)
**returns** | `Vnode` | | A fragment [vnode](vnodes.md#structure)
[How to read signatures](signatures.md)
@ -49,14 +49,14 @@ Argument | Type | Required | D
`m.fragment()` creates a [fragment vnode](vnodes.md) with attributes. It is meant for advanced use cases involving [keys](keys.md) or [lifecyle methods](lifecycle-methods.md).
A fragment vnode represents a list of DOM elements. If you want a regular element vnode that represents only one DOM element, you should use [`m()`](hyperscript.md) instead.
A fragment vnode represents a list of DOM elements. If you want a regular element vnode that represents only one DOM element and don't require keyed logic, you should use [`m()`](hyperscript.md) instead.
Normally you can use simple arrays instead to denote a list of nodes:
Normally you can use simple arrays or splats instead to denote a list of nodes:
```javascript
var groupVisible = true
m("ul", [
m("ul",
m("li", "child 1"),
m("li", "child 2"),
groupVisible ? [
@ -64,7 +64,7 @@ m("ul", [
m("li", "child 3"),
m("li", "child 4"),
] : null
])
)
```
However, Javascript arrays cannot be keyed or hold lifecycle methods. One option would be to create a wrapper element to host the key or lifecycle method, but sometimes it is not desirable to have an extra element (for example in complex table structures). In those cases, a fragment vnode can be used instead.

View file

@ -5,7 +5,7 @@
- [How it works](#how-it-works)
- [Flexibility](#flexibility)
- [CSS selectors](#css-selectors)
- [Attributes passed as the second argument](attributes-passed-as-the-second-argument)
- [Attributes passed as the second argument](#attributes-passed-as-the-second-argument)
- [DOM attributes](#dom-attributes)
- [Style attribute](#style-attribute)
- [Events](#events)
@ -40,12 +40,12 @@ You can also [use HTML syntax](https://babeljs.io/repl/#?code=%2F**%20%40jsx%20m
### Signature
`vnode = m(selector, attributes, children)`
`vnode = m(selector, attrs, children)`
Argument | Type | Required | Description
------------ | ------------------------------------------ | -------- | ---
`selector` | `String|Object` | Yes | A CSS selector or a [component](components.md)
`attributes` | `Object` | No | HTML attributes or element properties
`attrs` | `Object` | No | HTML attributes or element properties
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats)
**returns** | `Vnode` | | A [vnode](vnodes.md#structure)
@ -55,7 +55,7 @@ Argument | Type | Required | Descripti
### How it works
Mithril provides a hyperscript function `m()`, which allows expressing any HTML structure using javascript syntax. It accepts a `selector` string (required), an `attributes` object (optional) and a `children` array (optional).
Mithril provides a hyperscript function `m()`, which allows expressing any HTML structure using javascript syntax. It accepts a `selector` string (required), an `attrs` object (optional) and a `children` array (optional).
```javascript
m("div", {id: "box"}, "hello")
@ -262,6 +262,8 @@ m("div[style=background:red]")
Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.
You can use both hyphenated CSS property names (like `background-color`) and camel cased DOM `style` property names (like `backgroundColor`). You can also define [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables), if your browser supports them.
Mithril does not attempt to add units to number values. It simply stringifies them.
---
@ -539,7 +541,7 @@ Instead, prefer using Javascript expressions such as the ternary operator and Ar
```javascript
// PREFER
var BetterListComponent = {
view: function() {
view: function(vnode) {
return m("ul", vnode.attrs.items.map(function(item) {
return m("li", item)
}))

View file

@ -44,7 +44,7 @@ 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](framework-comparison.md) page.
Mithril supports browsers all the way back to IE9, no polyfills required.
Mithril supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required.
---

View file

@ -2,56 +2,123 @@
Integration with third party libraries or vanilla javascript code can be achieved via [lifecycle methods](lifecycle-methods.md).
## Example
## noUiSlider Example
```javascript
var FullCalendar = {
/** NoUiSlider wrapper component */
function Slider() {
var slider
oncreate: function (vnode) {
console.log('FullCalendar::oncreate')
$(vnode.dom).fullCalendar({
// put your initial options and callbacks here
})
Object.assign(vnode.attrs.parentState, {fullCalendarEl: vnode.dom})
},
// Consider that the lib will modify this parent element in the DOM (e.g. add dependent class attribute and values).
// As long as you return the same view results here, mithril will not
// overwrite the actual DOM because it's always comparing old and new VDOM
// before applying DOM updates.
view: function (vnode) {
return m('div')
},
onbeforeremove: function (vnode) {
// Run any destroy / cleanup methods here.
//E.g. $(vnode.state.fullCalendarEl).fullCalendar('destroy')
return {
oncreate: function(vnode) {
// Initialize 3rd party lib here
slider = noUiSlider.create(vnode.dom, {
start: 0,
range: {min: 0, max: 100}
})
slider.on('update', function(values) {
vnode.attrs.onChange(values[0])
m.redraw()
})
},
onremove: function() {
// Cleanup 3rd party lib on removal
slider.destroy()
},
view: function() {
return m('div')
}
}
}
m.mount(document.body, {
view: function (vnode) {
return [
m('h1', 'Calendar'),
m(FullCalendar, {parentState: vnode.state}),
m('button', {onclick: prev}, 'Mithril Button -'),
m('button', {onclick: next}, 'Mithril Button +')
/** Demo app component */
function Demo() {
var showSlider = false
var value = 0
]
function next() {
$(vnode.state.fullCalendarEl).fullCalendar('next')
return {
view: function() {
return m('.app',
m('p',
m('button',
{
type: 'button',
onclick: function() {
showSlider = !showSlider
}
},
showSlider ? "Destroy Slider" : "Create Slider"
)
),
showSlider && m(Slider, {
onChange: function(v) {
value = v
}
}),
m('p', value)
)
}
function prev() {
$(vnode.state.fullCalendarEl).fullCalendar('prev')
}
}
}
})
m.mount(document.body, Demo)
```
Running example [flems: FullCalendar](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHigjQGsACAJxigBeADog4xAJ6w4hGDGKjuhfmBEgSxAA5xEAel3UAJmgBWcfNSi0ArobBQM-C7Sy6MJjAA9d7AEZxdMGsoKGoMWDRDR10AZnwAdnwABkDg0PCmKN58LA4LODhRAD4QAF8KdGxcRAIzShp6RmYagDdHbgAxNIBhDMj2wW5gYTQR4WJ6an4MRkRuILRqYgh6bgAKFrRaQxgASiGxhWI6NDhaWHwrAHM1gHIukN6oft5EREnpxlvdw-GAEg2Wx2+EMLl2+CCjz6WTWw1GR3G+m4mmsxG4EhsvG4HAgy3C3FommW9Dg3AwkW4YRCvgw1E4pNk-F+xFKP1G8PGAHlfCYYEt8BgChArmhAdsYALiMReOZNI4mMQAMrEGYwChDSFQJ6ZRwAUSgc024pBLlZh3KY3hLQgMAA7nMFksVmh1kadvs4eNxvxiNZeC6sHdDBAWt9zRRLeN6L4YGBaPx+FhaC0YA7rItiS6xe6DhziEiAErpsloCTcHbiXi0Mu6SmwcnWTTcHDEQjbBkwJzM-QAt0S8SqiE9aF6qDgzXal5B+DS6th+GlEaL9lYHI2BhrUHUaw4Bj4XzbCTqz3Ea12tMZ52uoF7XNe6XyP0u5DM8aB26EACMt3Vt0nWW+CM8zfNYHi1EdeGPOV+AYZVVUNG98AHRhWSA+8QNuXxUQmNAfzvBEjkmdg6TmTR+BaV8WV-ABZXFlGgbgACFsNWABaQDKPfLCpXoPCT3QnDLAgEjuDQGBPAUYCqO4W5aNbXgGOYniXQAannZkAF1IyOR1M1E8TiDWD1KN7RDkIlCcIP1cdhwiGFbjEiT1KOZdmV0q8yJgFojPw+9TONcyhyhOzRxs4KdV4O5PNDNl71chdLVZMoKhATAcDwfIECoE4mmIPAyg0qh2C4BAUEqdKalyeToHqP1yBqDRtD0XR000TgrmcVwqvoqAAAFP3wAaAFZdG6hSoHwOoqEkTRqhAOpynKuak13PKqDqvBGp0fRWvazrRpcBVeoAJkGgBOfBjoO1bJqykAZrmhaUrSx6AEdrE7CRat4er1ClJqdrQNqOroVwTHez7eriU7P10YNxF0cGPt4CRbvqB68Cepa8E1KkIu+36tua3aQZcVIQjxl4oYSZI4YgBHcYgtHpokWbMYQUoNNKIA)
[Live Demo](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvEAXwvW10QICsEqdBk2J4A9ACoJAAgBytAKoQAylAgATGACdpAdy0YADoe3S6WQ-RHSJYgDpowAVzTViEetNUbtACgCU0sAO0tIAbhg6cGqaWg4h0lowxE5aaEEJofTUSRiMiNLOru70vmFotJqBwemhdWJi0gCSaBDuGGoAXjDSAMxa6tKGkcQAntJqAEbShNowmXXRPjoAvNIVSt6x+DkweTBlFZr46rRYFBm1dYvEIwUADBQL1wZoAOYwBcBYEGgPF1gMAAPAoARnu91Yz2krH80KW21KAHInIZ1PskRcim4PGgyh0nPBqtDQuVKjB8HliFo4Ph6ABhQgYd4HCJQQlwZD3AC6cKu12kWHwSXUBl0AWhsIW7AW9CSWFoYU+hRcONKxP5oQa0npsGZqL6AyGI3GU2knnlio68Ji2hO8GptFGEv5Mv5YQgMF0BWxJTxGoFiWSqXSWF8SPUEDCSL51yhtXj8YckhkABEYArpEZDGYzpY0NZbA5fbjpOmFQFLqFYMRpHBCLRdFtTGswB04PNajXwgSemt7vFakkUmkq3UPV6faq-ZWaoHhyHBeHKcZMSSl0jDGvNQKw0jJk5iMR6NvA4G52fA2MTAV94fj2hT5eBdk1NQANZT4q42fry-1xtm1WaQAEIAKbW04h3S942fOo3Tg0JwKA6QAH5pDsEB0zgR1xiAzDpAKTD6VyRgvEgzC-2kWMz38J5oLrBsIOWaQADJWKXICLgvS8GSZFkvzVPEwgDRC2UJaQ1jCKjYLPWF6MvPctwucSYBo651JhBJE0HIUFRcYhfFOagnBwBh8EmSpRguctaD5NgOBATAcDwHY4AEGh6EYZgeDYbkqDUNB3wQFBOBcngKicCAEW0SgQFScgeBIYhDDgRAGhcQx3zeHYzjESLosggABUEACZ8FBfB7jESMcK0CAD0YfLaCimKtHwfg4uvbgQDgHIIEMUR2DCnqCratyPISvBktS9KxEy7LcqwZrWuKsqKqqmroupBrDxgFbCuWCautGEw8Bw0ZYAcka8B+YhCHq8gqCmpKj1mjK0CynLzDEO6HugEqNoANl+tp-qgDqPO687+sGvzWCAA)
## Bootstrap FullCalandar Example
```javascript
/** FullCalendar wrapper component */
var FullCalendar = {
oncreate: function (vnode) {
console.log('FullCalndar::oncreate')
$(vnode.dom).fullCalendar({
// put your initial options and callbacks here
})
},
onremove: function (vnode) {
// Run any destroy / cleanup methods here.
$(vnode.dom).fullCalendar('destroy')
},
view: function (vnode) {
return m('div')
}
}
/** Demo app component */
function Demo() {
var fullCalendarEl
function next() {
$(fullCalendarEl).fullCalendar('next')
}
function prev() {
$(fullCalendarEl).fullCalendar('prev')
}
return {
view: function (vnode) {
return [
m('h1', 'Calendar'),
m(FullCalendar, {
oncreate: function(vnode) {
fullCalendarEl = vnode.dom
}
}),
m('button', {
onclick: prev
}, 'Mithril Button -'),
m('button', {
onclick: next
}, 'Mithril Button +')
]
}
}
}
m.mount(document.body, Demo)
```
[Live Demo](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHigjQGsACAJxigBeADog4xAJ6w4hGDGKjuhfmBEgSxAA5xEAel3UAJmgBWcfNSi0ArobBQM-C7Sy6MJjAA9d7AEZxdMGsoKGoMWDRDR10AZnwAdnwABkDg0PCmKN58LA4LODhRAD4QAF8KdGxcRAIzShp6RmYa3QAqVu4AMTSAYQzIx24Ad14MTU0YXm46LE16JmJuVt1hNAA3Qe6Qvois7kFuYFXhYnpqfgxGRG4gtGpiCHpuAAo1tFpDGABKQ+OFYjoaDgtFg+CsAHNngByLZQHYDXiIRBnC6MKFfP4nAAkr3en3whhcX3wQW2-SyzyOaBONOI+m4mmsiwkNimHAgD3C3Fomge9Dg3AwkWm4Sgvgw1E4Atk-ExxFKGOp8oof3o-CwtDWMGut3ujzQLzeH2+vyVJ3pACVrAahRJuJ9xLxaHbdNNYELrJpuDhiIQPtLJjB8HKcUb8YSsMTSXDyY5oQ7iE6JOi-uU-msIDAhjrrXqnrjjT8qbT+MRrLwDVh4xA1imlaVVg3qWg2h0ACIwDWC8bTFxzNALJYrNC6vkGjsa55F1bcbgbKbRnaZRwAUSgxwNN1zY+4A88xCnptns5xi9jvDXUd65+he+IddnTZnW7uO80-DWh6px+4p+vu1XKArzJADeGhd8YFrRVHw3WdS3LA1v2PDMsxzV99UNPETSQn94IrbhkGfH9ZyrKFCAARihChuChJcEXRFVN2I71nlhOismonDmO5O5UW1F88zQAtPmnJjuNnM9QLXfY5ywgkXCI7im3EhVGPE0jfCZU40Coo9xJ4ywIEla4ILWRSf3KGiAFkOWUaBuAAIS0p4AFoGPM48NOcnTOI8n8znYYzdxgfc-O4SyoRs31eHspziG07gAGo6w8gBdRTlPCxsNywHIbAYZ5CWoawcAYfBfA+CRqInWhFTKCoQEwHA8HyBAqEBJpiDwMpUqodguAQFBKmampcmi6B6nLcgag0bQ9F0a1NE4cFnFcMa7KgAABcj8B2gBWXR1piqB8DqKhJAmPA4HOCBeXq4bqhADVSq6qgprwWadH0RbltWw6XAWTaACZdoATnwIH-pe062pAC7HuumK7vKB68BMABHaxJgkSbeGm9R4rm760CWlaZl0DGsd4CRNriEHyN0QwIHECnMexmH6nhq6buRhqmse6MwlA3H8c++afrJlxUhCIXl14WmEmSRnmbpQXzw586JEumpEdurrSlS0ogA)

View file

@ -60,19 +60,24 @@ Like in other hooks, the `this` keyword in the `oninit` callback points to `vnod
The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`.
```javascript
var ComponentWithState = {
oninit: function(vnode) {
this.data = vnode.attrs.data
},
view: function() {
return m("div", this.data) // displays data from initialization time
function ComponentWithState() {
var initialData
return {
oninit: function(vnode) {
initialData = vnode.attrs.data
},
view: function(vnode) {
return [
// displays data from initialization time:
m("div", "Initial: " + initialData),
// displays current data:
m("div", "Current: " + vnode.attrs.data)
]
}
}
}
m(ComponentWithState, {data: "Hello"})
// Equivalent HTML
// <div>Hello</div>
```
You should not modify model data synchronously from this method. Since `oninit` makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle.
@ -110,17 +115,19 @@ The `onupdate(vnode)` hook is called after a DOM element is updated, while attac
This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled.
Like in other hooks, the `this` keyword in the `onupdate` callback points to `vnode.state`. DOM elements whose vnodes have an `onupdate` hook do not get recycled.
DOM elements whose vnodes have an `onupdate` hook do not get recycled.
The `onupdate` hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed.
```javascript
var RedrawReporter = {
count: 0,
onupdate: function(vnode) {
console.log("Redraws so far: ", ++vnode.state.count)
},
view: function() {}
function RedrawReporter() {
var count = 0
return {
onupdate: function() {
console.log("Redraws so far: ", ++count)
},
view: function() {}
}
}
m(RedrawReporter, {data: "Hello"})
@ -163,16 +170,17 @@ Like in other hooks, the `this` keyword in the `onremove` callback points to `vn
The `onremove` hook is useful for running clean up tasks.
```javascript
var Timer = {
oninit: function(vnode) {
this.timeout = setTimeout(function() {
console.log("timed out")
}, 1000)
},
onremove: function(vnode) {
clearTimeout(this.timeout)
},
view: function() {}
function Timer() {
var timeout = setTimeout(function() {
console.log("timed out")
}, 1000)
return {
onremove: function() {
clearTimeout(timeout)
},
view: function() {}
}
}
```

View file

@ -183,4 +183,4 @@ traverseDirectory("./docs", function(pathname) {
})
}
})
.then(process.exit)
.then(process.exit)

View file

@ -7,8 +7,6 @@
- [m.jsonp](jsonp.md)
- [m.parseQueryString](parseQueryString.md)
- [m.buildQueryString](buildQueryString.md)
- [m.withAttr](withAttr.md)
- [m.prop](prop.md)
- [m.trust](trust.md)
- [m.fragment](fragment.md)
- [m.redraw](redraw.md)

View file

@ -1,152 +0,0 @@
# prop(attrName, callback)
- [Description](#description)
- [Signature](#signature)
- [How it works](#how-it-works)
- [Sending through requests](#sending-through-requests)
---
### Description
Returns a simple getter/setter object.
```javascript
var name = m.prop("John")
var oldName = name.get() // First, it's set to "John"
name.set("Mary") // Set the value to "Mary"
var newName = name.get() // Now it's "Mary", not "John"
```
---
### Signature
`prop = m.prop(initial?)`
Argument | Type | Required | Description
----------- | ------ | -------- | ---
`initial` | `any` | No | The prop's initial value
**returns** | `Prop` | | A prop
`value = prop.get()`
Argument | Type | Required | Description
----------- | ----- | -------- | ---
**returns** | `any` | | The prop's current value
`newValue = prop.set(newValue)`
Argument | Type | Required | Description
----------- | ----- | -------- | ---
`newValue` | `any` | Yes | The value to set the prop to
**returns** | `any` | | The value you just set the prop to, for convenience
[How to read signatures](signatures.md)
---
### How it works
The `m.prop` method creates a prop, a getter/setter object wrapping a single mutable reference. You can get the current value with `prop.get()` and set it with `prop.set(value)`. Unlike [streams](stream.md), you can't observe them, so you can't do as much with them.
In conjunction with [`m.withAttr`](withAttr.md), you can emulate two-way binding pretty easily.
```javascript
var Component = {
oninit: function(vnode) {
vnode.state.current = m.prop("")
},
view: function(vnode) {
return m("input", {
oninput: m.withAttr("value", vnode.state.current.set),
value: vnode.state.current.get(),
})
}
}
```
They're also useful for making simpler models.
```javascript
// With props
var Auth = {
username: m.prop(""),
password: m.prop(""),
canSubmit: function() {
return Auth.username.get() !== "" && Auth.password.get() !== ""
},
login: function() {
// ...
},
}
// Without props
var Auth = {
username: "",
password: "",
setUsername: function(value) {
Auth.username = value
}
setPassword: function(value) {
Auth.password = value
}
canSubmit: function() {
return Auth.username !== "" && Auth.password !== ""
},
login: function() {
// ...
},
}
```
---
### Sending through requests
For convenience, props define `.toJSON` as an alias for `.get`. This is so you can send them through `m.request` without serializing them manually.
We could also take this model and simplify it:
```javascript
// How it's loaded
User.load = function(id) {
return m.request({
method: "GET",
url: "https://rem-rest-api.herokuapp.com/api/users/" + id,
withCredentials: true,
})
.then(function(result) {
User.current = {
id: result.id,
firstName: m.prop(result.firstName),
lastName: m.prop(result.lastName),
}
})
}
// Original
User.save = function(user) {
return m.request({
method: "PUT",
url: "https://rem-rest-api.herokuapp.com/api/users/" + user.id,
data: {
id: user.id,
firstName: user.firstName.get(),
lastName: user.lastName.get(),
},
withCredentials: true,
})
}
// Simplified
User.save = function(user) {
return m.request({
method: "PUT",
url: "https://rem-rest-api.herokuapp.com/api/users/" + user.id,
data: user,
withCredentials: true,
})
}
```

View file

@ -44,7 +44,7 @@ When callbacks outside of Mithril run, you need to notify Mithril's rendering en
To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw.
`m.redraw()` always triggers an asynchronous redraws, whereas `m.redraw.sync()` triggers a synchronous one. `m.redraw()` is tied to `window.requestAnimationFrame()` (we provide a fallback for IE9). It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.
`m.redraw()` always triggers an asynchronous redraws, whereas `m.redraw.sync()` triggers a synchronous one. `m.redraw()` is tied to `window.requestAnimationFrame()`. It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate.
`m.redraw.sync()` is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat:
@ -52,4 +52,4 @@ To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you u
- `m.redraw.sync()` called from an event handler can cause the DOM to be modified while an event is bubbling. Depending on the structure of the old and new DOM trees, the event can finish the bubbling phase in the new tree and trigger unwanted handlers.
- It is not throttled. One call to `m.redraw.sync()` causes immediately one `m.render()` call per root registered with [`m.mount()`](mount.md) or [`m.route()`](route.md).
`m.redraw()` doesn't have any of those issues, you can call it from wherever you like.
`m.redraw()` doesn't have any of those issues, you can call it from wherever you like.

View file

@ -4,6 +4,7 @@
- [Signature](#signature)
- [How it works](#how-it-works)
- [Typical usage](#typical-usage)
- [Error handling](#error-handling)
- [Loading icons and error messages](#loading-icons-and-error-messages)
- [Dynamic URLs](#dynamic-urls)
- [Aborting requests](#aborting-requests)
@ -127,6 +128,18 @@ When `m.route` is called at the bottom, the `Todos` component is initialized. `o
---
### Error handling
When a non-`file:` request returns with any status other than 2xx or 304, it rejects with an error. This error is a normal [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance, but with a few special properties.
- `error.message` is set to the raw response text.
- `error.code` is set to the status code itself.
- `error.response` is set to the parsed response, using `options.extract` and `options.deserialize` as is done with normal responses.
This is useful in many cases where errors are themselves things you can account for. If you want to detect if a session expired - you can do `if (error.code === 401) return promptForAuth().then(retry)`. If you hit an API's throttling mechanism and it returned an error with a `"timeout": 1000`, you could do a `setTimeout(retry, error.response.timeout)`.
---
### Loading icons and error messages
Here's an expanded version of the example above that implements a loading indicator and an error message:

View file

@ -78,6 +78,19 @@ Argument | Type | Required | Description
`options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call.
**returns** | | | Returns `undefined`
Remember that when using `.set` with params you also need to define the route:
```javascript
var Article = {
view: function(vnode) {
return "This is article " + vnode.attrs.articleid
}
}
m.route(document.body, {
'/article/:articleid': Article
})
m.route.set('/article/:articleid', {articleid: 1})
```
##### m.route.get
Returns the last fully resolved routing path, without the prefix. It may differ from the path displayed in the location bar while an asynchronous route is [pending resolution](#code-splitting).
@ -205,11 +218,11 @@ The routing strategy dictates how a library might actually implement routing. Th
- Using the querystring. A URL using this strategy typically looks like `http://localhost/?/page1`
- Using the pathname. A URL using this strategy typically looks like `http://localhost/page1`
Using the hash strategy is guaranteed to work in browsers that don't support `history.pushState` (namely, Internet Explorer 9), because it can fall back to using `onhashchange`. Use this strategy if you want to support IE9.
Using the hash strategy is guaranteed to work in browsers that don't support `history.pushState`, because it can fall back to using `onhashchange`. Use this strategy if you want to keep the hashes purely local.
The querystring strategy also technically works in IE9, but it falls back to reloading the page. Use this strategy if you want to support anchored links and you are not able to make the server-side necessary to support the pathname strategy.
The querystring strategy allows server-side detection, but it doesn't appear as a normal path. Use this strategy if you want to support and potentially detect anchored links server-side and you are not able to make the changes necessary to support the pathname strategy (like if you're using Apache and can't modify your .htaccess).
The pathname strategy produces the cleanest looking URLs, but does not work in IE9 *and* requires setting up the server to serve the single page application code from every URL that the application can route to. Use this strategy if you want cleaner-looking URLs and do not need to support IE9.
The pathname strategy produces the cleanest looking URLs, but requires setting up the server to serve the single page application code from every URL that the application can route to. Use this strategy if you want cleaner-looking URLs.
Single page applications that use the hash strategy often use the convention of having an exclamation mark after the hash to indicate that they're using the hash as a routing mechanism and not for the purposes of linking to anchors. The `#!` string is known as a *hashbang*.
@ -370,7 +383,10 @@ var Form = {
},
view: function() {
return m("form", [
m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
m("input[placeholder='Search']", {
oninput: function (e) { state.term = e.target.value },
value: state.term
}),
m("button", {onclick: state.search}, "Search")
])
}
@ -576,8 +592,14 @@ var Auth = {
var Login = {
view: function() {
return m("form", [
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
m("input[type=text]", {
oninput: function (e) { Auth.setUsername(e.target.value) },
value: Auth.username
}),
m("input[type=password]", {
oninput: function (e) { Auth.setPassword(e.target.value) },
value: Auth.password
}),
m("button[type=button]", {onclick: Auth.login}, "Login")
])
}

View file

@ -471,12 +471,12 @@ module.exports = {
}, [
m("label.label", "First name"),
m("input.input[type=text][placeholder=First name]", {
oninput: m.withAttr("value", function(value) {User.current.firstName = value}),
oninput: function (e) {User.current.firstName = e.target.value},
value: User.current.firstName
}),
m("label.label", "Last name"),
m("input.input[placeholder=Last name]", {
oninput: m.withAttr("value", function(value) {User.current.lastName = value}),
oninput: function (e) {User.current.lastName = e.target.value},
value: User.current.lastName
}),
m("button.button[type=submit]", "Save"),

View file

@ -7,7 +7,8 @@
- [Stream.merge](#streammerge)
- [Stream.scan](#streamscan)
- [Stream.scanMerge](#streamscanmerge)
- [Stream.HALT](#streamhalt)
- [Stream.lift](#streamlift)
- [Stream.SKIP](#streamskip)
- [Stream["fantasy-land/of"]](#streamfantasy-landof)
- [Instance members](#instance-members)
- [stream.map](#streammap)
@ -120,13 +121,13 @@ Argument | Type | Required | Description
Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value.
Note that you can prevent dependent streams from being updated by returning the special value `stream.HALT` inside the accumulator function.
Note that you can prevent dependent streams from being updated by returning the special value `stream.SKIP` inside the accumulator function.
`stream = Stream.scan(fn, accumulator, stream)`
Argument | Type | Required | Description
------------- | -------------------------------- | -------- | ---
`fn` | `(accumulator, value) -> result \| HALT` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value
`fn` | `(accumulator, value) -> result \| SKIP` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value of the same type
`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
@ -151,9 +152,40 @@ Argument | Type | Required | De
---
##### Stream.HALT
##### Stream.lift
A special value that can be returned to stream callbacks to halt execution of downstreams
Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams). Unlike `combine`, the input streams are a variable number of arguments (instead of an array) and the callback receives the stream values instead of streams. There is no `changed` parameter. This is generally a more user-friendly function for applications than `combine`.
`stream = Stream.lift(lifter, stream1, stream2, ...)`
Argument | Type | Required | Description
------------ | --------------------------- | -------- | ---
`lifter` | `(any...) -> any` | Yes | See [lifter](#lifter) argument
`streams...` | list of `Streams` | Yes | Streams to be lifted
**returns** | `Stream` | | Returns a stream
[How to read signatures](signatures.md)
---
###### lifter
Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams)
`any = lifter(streams...)`
Argument | Type | Required | Description
------------ | -------------------- | -------- | ---
`streams...` | splat of `Streams` | No | Splat of zero or more streams that correspond to the streams passed to [`stream.lift`](#stream-lift)
**returns** | `any` | | Returns a computed value
[How to read signatures](signatures.md)
---
##### Stream.SKIP
A special value that can be returned to stream callbacks to skip execution of downstreams
---
@ -274,7 +306,7 @@ In the example above, the `users` stream is populated with the response data whe
#### Bidirectional bindings
Streams can also be populated from other higher order functions, such as [`m.withAttr`](withAttr.md)
Streams can also be populated from event callbacks and similar.
```javascript
// a stream
@ -282,7 +314,7 @@ var user = stream("")
// a bi-directional binding to the stream
m("input", {
oninput: m.withAttr("value", user),
oninput: function (e) { user(e.target.value) },
value: user()
})
```
@ -343,14 +375,14 @@ console.log(doubled()) // logs 2
Dependent streams are *reactive*: their values are updated any time the value of their parent stream is updated. This happens regardless of whether the dependent stream was created before or after the value of the parent stream was set.
You can prevent dependent streams from being updated by returning the special value `stream.HALT`
You can prevent dependent streams from being updated by returning the special value `stream.SKIP`
```javascript
var halted = stream(1).map(function(value) {
return stream.HALT
var skipped = stream(1).map(function(value) {
return stream.SKIP
})
halted.map(function() {
skipped.map(function() {
// never runs
})
```
@ -372,6 +404,18 @@ var greeting = stream.merge([a, b]).map(function(values) {
console.log(greeting()) // logs "hello world"
```
Or you can use the helper function `stream.lift()`
```javascript
var a = stream("hello")
var b = stream("world")
var greeting = stream.lift(function(_a, _b) {
return _a + " " + _b
}, a, b)
console.log(greeting()) // logs "hello world"
```
There's also a lower level method called `stream.combine()` that exposes the stream themselves in the reactive computations for more advanced use cases
@ -388,14 +432,14 @@ console.log(added()) // logs 12
A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also brings the performance benefits of not recomputing downstreams unnecessarily.
You can prevent dependent streams from being updated by returning the special value `stream.HALT`
You can prevent dependent streams from being updated by returning the special value `stream.SKIP`
```javascript
var halted = stream.combine(function(stream) {
return stream.HALT
var skipped = stream.combine(function(stream) {
return stream.SKIP
}, [stream(1)])
halted.map(function() {
skipped.map(function() {
// never runs
})
```

View file

@ -21,7 +21,7 @@ It may seem wasteful to recreate vnodes so frequently, but as it turns out, mode
For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.
The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide a declarative [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics)) API, a style of rendering that makes it drastically easier to manage UI complexity.
The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide a declarative [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics%29) API, a style of rendering that makes it drastically easier to manage UI complexity.
To illustrate why immediate mode is so important, consider the DOM API and HTML. The DOM API is an imperative [retained mode](https://en.wikipedia.org/wiki/Retained_mode) API and requires 1. writing out exact instructions to assemble a DOM tree procedurally, and 2. writing out other instructions to update that tree. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances of introducing bugs and more chances to make code harder to understand.

View file

@ -1,136 +0,0 @@
# withAttr(attrName, callback)
- [Description](#description)
- [Signature](#signature)
- [How it works](#how-it-works)
- [Predictable event target](#predictable-event-target)
- [Attributes and properties](#attributes-and-properties)
---
### Description
Returns an event handler that runs `callback` with the value of the specified DOM attribute
```javascript
var state = {
value: "",
setValue: function(v) {state.value = v}
}
var Component = {
view: function() {
return m("input", {
oninput: m.withAttr("value", state.setValue),
value: state.value,
})
}
}
m.mount(document.body, Component)
```
---
### Signature
`m.withAttr(attrName, callback, thisArg?)`
Argument | Type | Required | Description
----------- | -------------------- | -------- | ---
`attrName` | `String` | Yes | The name of the attribute or property whose value will be used
`callback` | `any -> undefined` | Yes | The callback
`thisArg` | `any` | No | An object to bind to the `this` keyword in the callback function
**returns** | `Event -> undefined` | | An event handler function
[How to read signatures](signatures.md)
---
### How it works
The `m.withAttr` method creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument.
This helper function is provided to help decouple the browser's event model from application code.
```javascript
// standalone usage
document.body.onclick = m.withAttr("title", function(value) {
console.log(value) // logs the title of the <body> element when clicked
})
```
Typically, `m.withAttr()` can be used in Mithril component views to avoid polluting the data layer with DOM event model concerns:
```javascript
var state = {
email: "",
setEmail: function(email) {
state.email = email.toLowerCase()
}
}
var MyComponent = {
view: function() {
return m("input", {
oninput: m.withAttr("value", state.setEmail),
value: state.email
})
}
}
m.mount(document.body, MyComponent)
```
---
### Predictable event target
The `m.withAttr()` helper reads the value of the element to which the event handler is bound, which is not necessarily the same as the element where the event originated.
```javascript
var state = {
url: "",
setURL: function(url) {state.url = url}
}
var MyComponent = {
view: function() {
return m("a[href='/foo']", {onclick: m.withAttr("href", state.setURL)}, [
m("span", state.url)
])
}
}
m.mount(document.body, MyComponent)
```
In the example above, if the user clicks on the text within the link, `e.target` will point to the `<span>`, not the `<a>`.
While this behavior works as per its specs, it's not very intuitive or useful most of the time. Therefore, `m.withAttr` uses the value of `e.currentTarget` which does point to the `<a>`, as one would normally expect.
---
### Attributes and properties
The first argument of `m.withAttr()` can be either an attribute or a property.
```javascript
// reads from `select.selectedIndex` property
var state = {
index: 0,
setIndex: function(index) {state.index = index}
}
m("select", {onclick: m.withAttr("selectedIndex", state.setIndex)})
```
If a value can be both an attribute *and* a property, the property value is used.
```javascript
// value is a boolean, because the `input.checked` property is boolean
var state = {
selected: false,
setSelected: function(selected) {state.selected = selected}
}
m("input[type=checkbox]", {onclick: m.withAttr("checked", state.setSelected)})
```

67
esm.js Normal file
View file

@ -0,0 +1,67 @@
"use strict"
/*
This script will create esm compatible scripts
from the already compiled versions of:
- mithril.js > mithril.mjs
- mithril.min.js > mithril.min.mjs
- /stream/stream.js > stream.mjs
*/
var fs = require("fs")
var namedExports = [
"m",
"trust",
"fragment",
"mount",
"route",
"render",
"redraw",
"request",
"jsonp",
"parseQueryString",
"buildQueryString",
"version",
"vnode",
"PromisePolyfill"
]
var mithril = fs.readFileSync("mithril.js", "utf8")
fs.writeFileSync("mithril.mjs",
mithril.slice(
mithril.indexOf("\"use strict\"") + 13,
mithril.lastIndexOf("if (typeof module")
)
+ "\nexport default m"
// The exports are declared with prefixed underscores to avoid overwriting previously
// declared variables with the same name
+ "\nvar " + namedExports.map(function(n) { return "_" + n + " = m." + n }).join(",")
+ "\nexport {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "}"
)
var mithrilMin = fs.readFileSync("mithril.min.js", "utf8")
var mName = mithrilMin.match(/window\.m=([a-z])}/)[1]
fs.writeFileSync("mithril.min.mjs",
mithrilMin.slice(
12,
mithrilMin.lastIndexOf("\"undefined\"!==typeof module")
)
+ "export default " + mName + ";"
// The exports are declared with prefixed underscores to avoid overwriting previously
// declared variables with the same name
+ "var " + namedExports.map(function(n) { return "_" + n + "=m." + n }).join(",") + ";"
+ "export {" + namedExports.map(function(n) { return "_" + n + " as " + n }).join(",") + "};"
)
var stream = fs.readFileSync("stream/stream.js", "utf8")
fs.writeFileSync("stream/stream.mjs",
stream.slice(
stream.indexOf("\"use strict\"") + 13,
stream.lastIndexOf("if (typeof module")
)
+ "\nexport default Stream"
)

View file

@ -28,7 +28,7 @@ var Editor = {
view: function() {
return [
m("textarea.input", {
oninput: m.withAttr("value", state.update),
oninput: function (e) { state.update(e.traget.value) },
value: state.text
}),
m(".preview", m.trust(marked(state.text))),

View file

@ -1,6 +1,11 @@
"use strict"
var m = require("./hyperscript")
var hyperscript = require("./hyperscript")
var m = function m() { return hyperscript.apply(this, arguments) }
m.m = hyperscript
m.trust = hyperscript.trust
m.fragment = hyperscript.fragment
var requestService = require("./request")
var redrawService = require("./redraw")
@ -8,8 +13,6 @@ requestService.setCompletionCallback(redrawService.redraw)
m.mount = require("./mount")
m.route = require("./route")
m.withAttr = require("./util/withAttr")
m.prop = require("./util/prop")
m.render = require("./render").render
m.redraw = redrawService.redraw
m.request = requestService.request

1038
mithril.js

File diff suppressed because it is too large Load diff

2
mithril.min.js vendored

File diff suppressed because one or more lines are too long

1
mithril.min.mjs Normal file

File diff suppressed because one or more lines are too long

1583
mithril.mjs Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,8 @@ window.module = {
set exports(value) {require.$$modules[require.$$current()] = value},
}
window.global = window
function require(name) {
var relative = require.$$current()
var slashIndex = relative.lastIndexOf("/")

View file

@ -429,7 +429,7 @@ If an argument is defined for the `assertions` function, the test is deemed to b
### Assertion o(any value)
Starts an assertion. There are four types of assertion: `equals`, `notEquals`, `deepEquals` and `notDeepEquals`.
Starts an assertion. There are six types of assertion: `equals`, `notEquals`, `deepEquals`, `notDeepEquals`, `throws`, `notThrows`.
Assertions have this form:
@ -467,6 +467,22 @@ Asserts that two values are recursively equal
Asserts that two values are not recursively equal
#### Function(String description) o(Function fn).throws(Object constructor)
Asserts that a function throws an instance of the provided constructo
#### Function(String description) o(Function fn).throws(String message)
Asserts that a function throws an Error with the provided message
#### Function(String description) o(Function fn).notThrows(Object constructor)
Asserts that a function does not throw an instance of the provided constructor
#### Function(String description) o(Function fn).notThrows(String message)
Asserts that a function does not throw an Error with the provided message
---
### void o.before(Function([Function done [, Function timeout]]) setup)
@ -617,7 +633,7 @@ o.spec("message", function() {
### String result.context
In case of failure, a `>`-separated string showing the structure of the test specification.
A `>`-separated string showing the structure of the test specification.
In the below example, `result.context` would be `testing > rocks`.
```javascript

View file

@ -3,8 +3,10 @@
## Upcoming...
_2018-xx-yy_
- ospec: Test results now include `.message` and `.context` regardless of whether the test passed or failed. (#2227 @robertakarobin)
<!-- Add new lines here. Version number will be decided later -->
- Add `spy.calls` array property to get the `this` and `arguments` values for any arbitrary call.
- Added `.throws` and `.notThrows` assertions to ospec. (#2255 @robertakarobin)
## 3.0.1
_2018-06-30_

19
ospec/esm.js Normal file
View file

@ -0,0 +1,19 @@
"use strict"
/*
This script will create an esm compatible script
from the already compiled version of:
- ospec.js > ospec.mjs
*/
var fs = require("fs")
var ospec = fs.readFileSync("ospec.js", "utf8")
fs.writeFileSync("ospec.mjs",
"export default "
+ ospec.slice(ospec.indexOf("})") + 2)
+ "()"
)

View file

@ -220,6 +220,8 @@ else window.o = m()
define("notEquals", "should not equal", function(a, b) {return a !== b})
define("deepEquals", "should deep equal", deepEqual)
define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)})
define("throws", "should throw a", throws)
define("notThrows", "should not throw a", function(a, b) {return !throws(a, b)})
function isArguments(a) {
if ("callee" in a) {
@ -260,6 +262,18 @@ else window.o = m()
}
return false
}
function throws(a, b){
try{
a()
}catch(e){
if(typeof b === "string"){
return (e.message === b)
}else{
return (e instanceof b)
}
}
return false
}
function isRunning() {return results != null}
function Assert(value) {
@ -273,16 +287,20 @@ else window.o = m()
}
function define(name, verb, compare) {
Assert.prototype[name] = function assert(value) {
if (compare(this.value, value)) succeed(this)
else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value))
var self = this
return function(message) {
if (!self.pass) self.message = message + "\n\n" + self.message
}
var message = serialize(self.value) + "\n " + verb + "\n" + serialize(value)
if (compare(self.value, value)){
succeed(self, message)
return function(message) {
if (!self.pass) self.message = message + "\n\n" + self.message
}
}else fail(self, message)
}
}
function succeed(assertion) {
function succeed(assertion, message) {
results[assertion.i].pass = true
results[assertion.i].context = subjects.join(" > ")
results[assertion.i].message = message
}
function fail(assertion, message, error) {
results[assertion.i].pass = false

363
ospec/ospec.mjs Normal file
View file

@ -0,0 +1,363 @@
export default (function init(name) {
var spec = {}, subjects = [], results, only = [], ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty
var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/), timeoutStackName
var globalTimeout = noTimeoutRightNow
var currentTestError = null
if (name != null) spec[name] = ctx = {}
function o(subject, predicate) {
if (predicate === undefined) {
if (!isRunning()) throw new Error("Assertions should not occur outside test definitions")
return new Assert(subject)
} else {
if (isRunning()) throw new Error("Test definitions and hooks shouldn't be nested. To group tests use `o.spec()`")
subject = String(subject)
if (subject.charCodeAt(0) === 1) throw new Error("test names starting with '\\x01' are reserved for internal use")
ctx[unique(subject)] = new Task(predicate, ensureStackTrace(new Error))
}
}
o.before = hook("\x01before")
o.after = hook("\x01after")
o.beforeEach = hook("\x01beforeEach")
o.afterEach = hook("\x01afterEach")
o.specTimeout = function (t) {
if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()")
if (hasOwn.call(ctx, "\x01specTimeout")) throw new Error("A default timeout has already been defined in this context")
if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument")
ctx["\x01specTimeout"] = t
}
o.new = init
o.spec = function(subject, predicate) {
var parent = ctx
ctx = ctx[unique(subject)] = {}
predicate()
ctx = parent
}
o.only = function(subject, predicate, silent) {
if (!silent) console.log(
highlight("/!\\ WARNING /!\\ o.only() mode") + "\n" + o.cleanStackTrace(ensureStackTrace(new Error)) + "\n",
cStyle("red"), ""
)
only.push(predicate)
o(subject, predicate)
}
o.spy = function(fn) {
var spy = function() {
spy.this = this
spy.args = [].slice.call(arguments)
spy.callCount++
if (fn) return fn.apply(this, arguments)
}
if (fn)
Object.defineProperties(spy, {
length: {value: fn.length},
name: {value: fn.name}
})
spy.args = []
spy.callCount = 0
return spy
}
o.cleanStackTrace = function(error) {
// For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack
if (error.stack == null) return ""
var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack
// some environments add the name and message to the stack trace
if (error.stack.indexOf(header) === 0) {
stack = error.stack.slice(header.length).split(/\r?\n/)
stack.shift() // drop the initial empty string
} else {
stack = error.stack.split(/\r?\n/)
}
if (ospecFileName == null) return stack.join("\n")
// skip ospec-related entries on the stack
while (stack[i] != null && stack[i].indexOf(ospecFileName) !== -1) i++
// now we're in user code (or past the stack end)
return stack[i]
}
o.timeout = function(n) {
globalTimeout(n)
}
o.run = function(reporter) {
results = []
start = new Date
test(spec, [], [], new Task(function() {
setTimeout(function () {
timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([\w \.]+?:\d+:\d+)/)
if (typeof reporter === "function") reporter(results)
else {
var errCount = o.report(results)
if (hasProcess && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit
}
})
}, null), 200 /*default timeout delay*/)
function test(spec, pre, post, finalize, defaultDelay) {
if (hasOwn.call(spec, "\x01specTimeout")) defaultDelay = spec["\x01specTimeout"]
pre = [].concat(pre, spec["\x01beforeEach"] || [])
post = [].concat(spec["\x01afterEach"] || [], post)
series([].concat(spec["\x01before"] || [], Object.keys(spec).reduce(function(tasks, key) {
if (key.charCodeAt(0) !== 1 && (only.length === 0 || only.indexOf(spec[key].fn) !== -1 || !(spec[key] instanceof Task))) {
tasks.push(new Task(function(done) {
o.timeout(Infinity)
subjects.push(key)
var pop = new Task(function pop() {subjects.pop(), done()}, null)
if (spec[key] instanceof Task) series([].concat(pre, spec[key], post, pop), defaultDelay)
else test(spec[key], pre, post, pop, defaultDelay)
}, null))
}
return tasks
}, []), spec["\x01after"] || [], finalize), defaultDelay)
}
function series(tasks, defaultDelay) {
var cursor = 0
next()
function next() {
if (cursor === tasks.length) return
var task = tasks[cursor++]
var fn = task.fn
currentTestError = task.err
var timeout = 0, delay = defaultDelay, s = new Date
var current = cursor
var arg
globalTimeout = setDelay
var isDone = false
// public API, may only be called once from use code (or after returned Promise resolution)
function done(err) {
if (!isDone) isDone = true
else throw new Error("`" + arg + "()` should only be called once")
if (timeout === undefined) console.warn("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms\n" + o.cleanStackTrace(task.err))
finalizeAsync(err)
}
// for internal use only
function finalizeAsync(err) {
if (err == null) {
if (task.err != null) succeed(new Assert)
} else {
if (err instanceof Error) fail(new Assert, err.message, err)
else fail(new Assert, String(err), null)
}
if (timeout !== undefined) timeout = clearTimeout(timeout)
if (current === cursor) next()
}
function startTimer() {
timeout = setTimeout(function() {
timeout = undefined
finalizeAsync("async test timed out after " + delay + "ms")
}, Math.min(delay, 2147483647))
}
function setDelay (t) {
if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument")
delay = t
}
if (fn.length > 0) {
var body = fn.toString()
arg = (body.match(/^(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*=>/) || body.match(/\((?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*(.+?)(?:\s|\/\*[\s\S]*?\*\/|\/\/.*?\n)*[,\)]/) || []).pop()
if (body.indexOf(arg) === body.lastIndexOf(arg)) {
var e = new Error
e.stack = "`" + arg + "()` should be called at least once\n" + o.cleanStackTrace(task.err)
throw e
}
try {
fn(done, setDelay)
}
catch (e) {
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
if (timeout === 0) {
startTimer()
}
} else {
try{
var p = fn()
if (p && p.then) {
startTimer()
p.then(function() { done() }, done)
} else {
nextTickish(next)
}
} catch (e) {
if (task.err != null) finalizeAsync(e)
// The errors of internal tasks (which don't have an Err) are ospec bugs and must be rethrown.
else throw e
}
}
globalTimeout = noTimeoutRightNow
}
}
}
function unique(subject) {
if (hasOwn.call(ctx, subject)) {
console.warn("A test or a spec named `" + subject + "` was already defined")
while (hasOwn.call(ctx, subject)) subject += "*"
}
return subject
}
function hook(name) {
return function(predicate) {
if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate)
ctx[name] = new Task(predicate, ensureStackTrace(new Error))
}
}
define("equals", "should equal", function(a, b) {return a === b})
define("notEquals", "should not equal", function(a, b) {return a !== b})
define("deepEquals", "should deep equal", deepEqual)
define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)})
function isArguments(a) {
if ("callee" in a) {
for (var i in a) if (i === "callee") return false
return true
}
}
function deepEqual(a, b) {
if (a === b) return true
if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise
if (typeof a === "object" && typeof b === "object") {
var aIsArgs = isArguments(a), bIsArgs = isArguments(b)
if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) {
for (var i in a) {
if ((!(i in b)) || !deepEqual(a[i], b[i])) return false
}
for (var i in b) {
if (!(i in a)) return false
}
return true
}
if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) {
var aKeys = Object.getOwnPropertyNames(a), bKeys = Object.getOwnPropertyNames(b)
if (aKeys.length !== bKeys.length) return false
for (var i = 0; i < aKeys.length; i++) {
if (!hasOwn.call(b, aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false
}
return true
}
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer) {
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
if (a.valueOf() === b.valueOf()) return true
}
return false
}
function isRunning() {return results != null}
function Assert(value) {
this.value = value
this.i = results.length
results.push({pass: null, context: "", message: "Incomplete assertion in the test definition starting at...", error: currentTestError, testError: currentTestError})
}
function Task(fn, err) {
this.fn = fn
this.err = err
}
function define(name, verb, compare) {
Assert.prototype[name] = function assert(value) {
if (compare(this.value, value)) succeed(this)
else fail(this, serialize(this.value) + "\n " + verb + "\n" + serialize(value))
var self = this
return function(message) {
if (!self.pass) self.message = message + "\n\n" + self.message
}
}
}
function succeed(assertion) {
results[assertion.i].pass = true
}
function fail(assertion, message, error) {
results[assertion.i].pass = false
results[assertion.i].context = subjects.join(" > ")
results[assertion.i].message = message
results[assertion.i].error = error != null ? error : ensureStackTrace(new Error)
}
function serialize(value) {
if (hasProcess) return require("util").inspect(value) // eslint-disable-line global-require
if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value)
else if (typeof value === "function") return value.name || "<anonymous function>"
try {return JSON.stringify(value)} catch (e) {return String(value)}
}
function noTimeoutRightNow() {
throw new Error("o.timeout must be called snchronously from within a test definition or a hook")
}
var colorCodes = {
red: "31m",
red2: "31;1m",
green: "32;1m"
}
function highlight(message, color) {
var code = colorCodes[color] || colorCodes.red;
return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + message + "\x1b[0m" : message) : "%c" + message + "%c "
}
function cStyle(color, bold) {
return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "")
}
function ensureStackTrace(error) {
// mandatory to get a stack in IE 10 and 11 (and maybe other envs?)
if (error.stack === undefined) try { throw error } catch(e) {return e}
else return error
}
function getStackName(e, exp) {
return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null
}
o.report = function (results) {
var errCount = 0
for (var i = 0, r; r = results[i]; i++) {
if (r.pass == null) {
r.testError.stack = r.message + "\n" + o.cleanStackTrace(r.testError)
r.testError.message = r.message
throw r.testError
}
if (!r.pass) {
var stackTrace = o.cleanStackTrace(r.error)
var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1
if (couldHaveABetterStackTrace) stackTrace = r.testError != null ? o.cleanStackTrace(r.testError) : r.error.stack || ""
console.error(
(hasProcess ? "\n" : "") +
highlight(r.context + ":", "red2") + "\n" +
highlight(r.message, "red") +
(stackTrace ? "\n" + stackTrace + "\n" : ""),
cStyle("black", true), "", // reset to default
cStyle("red"), cStyle("black")
)
errCount++
}
}
var pl = results.length === 1 ? "" : "s"
var resultSummary = (errCount === 0) ?
highlight((pl ? "All " : "The ") + results.length + " assertion" + pl + " passed", "green"):
highlight(errCount + " out of " + results.length + " assertion" + pl + " failed", "red2")
var runningTime = " in " + Math.round(Date.now() - start) + "ms"
console.log(
(hasProcess ? "\n" : "") +
(name ? name + ": " : "") + resultSummary + runningTime,
cStyle((errCount === 0 ? "green" : "red"), true), ""
)
return errCount
}
if (hasProcess) {
nextTickish = process.nextTick
} else {
nextTickish = function fakeFastNextTick(next) {
if (stack++ < 5000) next()
else setTimeout(next, stack = 0)
}
}
return o
})
()

View file

@ -3,6 +3,7 @@
"version": "3.0.1",
"description": "Noiseless testing framework",
"main": "ospec.js",
"module": "ospec.mjs",
"directories": {
"test": "tests"
},
@ -12,6 +13,9 @@
"bin": {
"ospec": "./bin/ospec"
},
"scripts": {
"prepublishOnly": "node esm.js"
},
"repository": "MithrilJS/mithril.js",
"dependencies": {
"glob": "^7.1.2"

View file

@ -89,6 +89,8 @@ o.spec("reporting", function() {
o(results.length).equals(2)("Two results")
o("error" in results[0] && "pass" in results[0]).equals(true)("error and pass keys present in failing result")
o("message" in results[0] && "context" in results[0]).equals(true)("message and context keys present in failing result")
o("message" in results[1] && "context" in results[1]).equals(true)("message and context keys present in passing result")
o(results[0].pass).equals(false)("Test meant to fail has failed")
o(results[1].pass).equals(true)("Test meant to pass has passed")
@ -163,6 +165,14 @@ o.spec("ospec", function() {
o(a).notEquals(2)
o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3})
o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}])
o(function(){throw new Error()}).throws(Error)
o(function(){"ayy".foo()}).throws(TypeError)
o(function(){Math.PI.toFixed(Math.pow(10,20))}).throws(RangeError)
o(function(){decodeURIComponent("%")}).throws(URIError)
o(function(){"ayy".foo()}).notThrows(SyntaxError)
o(function(){throw new Error("foo")}).throws("foo")
o(function(){throw new Error("foo")}).notThrows("bar")
var undef1 = {undef: void 0}
var undef2 = {UNDEF: void 0}
@ -666,7 +676,7 @@ o.spec("the done parser", function() {
var threw = false
oo("test", function(/*hey
*/ /**/ //ho
done /*hey
done /*hey
*/ /**/ //huuu
, timeout
) {

2732
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,18 @@
{
"name": "mithril",
"version": "2.0.0-rc.1",
"version": "2.0.0-rc.2",
"description": "A framework for building brilliant applications",
"author": "Leo Horie",
"license": "MIT",
"main": "mithril.js",
"module": "mithril.mjs",
"repository": "MithrilJS/mithril.js",
"scripts": {
"dev": "node bundler/cli browser.js -output mithril.js -watch",
"build": "npm run build-browser & npm run build-min",
"build": "npm run build-browser & npm run build-min && npm run build-esm",
"build-browser": "node bundler/cli browser.js -output mithril.js",
"build-min": "node bundler/cli browser.js -output mithril.min.js -minify",
"build-esm": "node esm.js",
"precommit": "lint-staged",
"lintdocs": "node docs/lint",
"gendocs": "node docs/generate",
@ -22,14 +24,15 @@
"cover": "istanbul cover --print both ospec/bin/ospec",
"release": "npm version -m 'v%s'",
"preversion": "npm run test",
"version": "npm run build && git add mithril.js mithril.min.js",
"version": "npm run build && git add mithril.js mithril.min.js mithril.mjs mithril.min.mjs",
"postversion": "git push --follow-tags"
},
"devDependencies": {
"@alrra/travis-scripts": "^3.0.1",
"benchmark": "^2.1.4",
"chokidar": "^2.0.4",
"dedent": "^0.7.0",
"eslint": "^3.19.0",
"eslint": "^5.9.0",
"gh-pages": "^0.12.0",
"glob": "^7.1.2",
"istanbul": "^0.4.5",
@ -37,7 +40,7 @@
"locater": "^1.3.0",
"marked": "^0.3.19",
"pinpoint": "^1.1.0",
"uglify-es": "^3.3.9"
"terser": "^3.10.11"
},
"bin": {
"ospec": "./ospec/bin/ospec"

View file

@ -1,7 +1,12 @@
"use strict"
var Vnode = require("../render/vnode")
var hyperscriptVnode = require("./hyperscriptVnode")
module.exports = function(attrs, children) {
return Vnode("[", attrs.key, attrs, Vnode.normalizeChildren(children), undefined, undefined)
module.exports = function() {
var vnode = hyperscriptVnode.apply(0, arguments)
vnode.tag = "["
vnode.children = Vnode.normalizeChildren(vnode.children)
return vnode
}

View file

@ -1,6 +1,7 @@
"use strict"
var Vnode = require("../render/vnode")
var hyperscriptVnode = require("./hyperscriptVnode")
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
var selectorCache = {}
@ -29,18 +30,21 @@ function compileSelector(selector) {
return selectorCache[selector] = {tag: tag, attrs: attrs}
}
function execSelector(state, attrs, children) {
var hasAttrs = false, childList, text
var classAttr = hasOwn.call(attrs, "class") ? "class" : "className"
var className = attrs[classAttr]
function execSelector(state, vnode) {
var attrs = vnode.attrs
var children = Vnode.normalizeChildren(vnode.children)
var hasClass = hasOwn.call(attrs, "class")
var className = hasClass ? attrs.class : attrs.className
vnode.tag = state.tag
vnode.attrs = null
vnode.children = undefined
if (!isEmpty(state.attrs) && !isEmpty(attrs)) {
var newAttrs = {}
for(var key in attrs) {
if (hasOwn.call(attrs, key)) {
newAttrs[key] = attrs[key]
}
for (var key in attrs) {
if (hasOwn.call(attrs, key)) newAttrs[key] = attrs[key]
}
attrs = newAttrs
@ -54,28 +58,28 @@ function execSelector(state, attrs, children) {
if (className != null || state.attrs.className != null) attrs.className =
className != null
? state.attrs.className != null
? state.attrs.className + " " + className
? String(state.attrs.className) + " " + String(className)
: className
: state.attrs.className != null
? state.attrs.className
: null
if (classAttr === "class") attrs.class = null
if (hasClass) attrs.class = null
for (var key in attrs) {
if (hasOwn.call(attrs, key) && key !== "key") {
hasAttrs = true
vnode.attrs = attrs
break
}
}
if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") {
text = children[0].children
vnode.text = children[0].children
} else {
childList = children
vnode.children = children
}
return Vnode(state.tag, attrs.key, hasAttrs ? attrs : null, childList, text)
return vnode
}
function hyperscript(selector) {
@ -83,27 +87,13 @@ function hyperscript(selector) {
throw Error("The selector must be either a string or a component.");
}
var attrs = arguments[1], start = 2, children
if (attrs == null) {
attrs = {}
} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
attrs = {}
start = 1
}
if (arguments.length === start + 1) {
children = arguments[start]
if (!Array.isArray(children)) children = [children]
} else {
children = []
while (start < arguments.length) children.push(arguments[start++])
}
var vnode = hyperscriptVnode.apply(1, arguments)
if (typeof selector === "string") {
return execSelector(selectorCache[selector] || compileSelector(selector), attrs, Vnode.normalizeChildren(children))
return execSelector(selectorCache[selector] || compileSelector(selector), vnode)
} else {
return Vnode(selector, attrs.key, attrs, children)
vnode.tag = selector
return vnode
}
}

View file

@ -0,0 +1,53 @@
"use strict"
var Vnode = require("../render/vnode")
// Call via `hyperscriptVnode.apply(startOffset, arguments)`
//
// The reason I do it this way, forwarding the arguments and passing the start
// offset in `this`, is so I don't have to create a temporary array in a
// performance-critical path.
//
// In native ES6, I'd instead add a final `...args` parameter to the
// `hyperscript` and `fragment` factories and define this as
// `hyperscriptVnode(...args)`, since modern engines do optimize that away. But
// ES5 (what Mithril requires thanks to IE support) doesn't give me that luxury,
// and engines aren't nearly intelligent enough to do either of these:
//
// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to
// another function only to be indexed.
// 2. Elide an `arguments` allocation when it's passed to any function other
// than `Function.prototype.apply` or `Reflect.apply`.
//
// In ES6, it'd probably look closer to this (I'd need to profile it, though):
// module.exports = function(attrs, ...children) {
// if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) {
// if (children.length === 1 && Array.isArray(children[0])) children = children[0]
// } else {
// children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children]
// attrs = undefined
// }
//
// if (attrs == null) attrs = {}
// return Vnode("", attrs.key, attrs, children)
// }
module.exports = function() {
var attrs = arguments[this], start = this + 1, children
if (attrs == null) {
attrs = {}
} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
attrs = {}
start = this
}
if (arguments.length === start + 1) {
children = arguments[start]
if (!Array.isArray(children)) children = [children]
} else {
children = []
while (start < arguments.length) children.push(arguments[start++])
}
return Vnode("", attrs.key, attrs, children)
}

View file

@ -10,8 +10,8 @@ module.exports = function($window) {
math: "http://www.w3.org/1998/Math/MathML"
}
var onevent
function setEventCallback(callback) {return onevent = callback}
var redraw
function setRedraw(callback) {return redraw = callback}
function getNameSpace(vnode) {
return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
@ -35,6 +35,15 @@ module.exports = function($window) {
}
}
// IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when
// inside an iframe. Catch and swallow this error, and heavy-handidly return null.
function activeElement() {
try {
return $doc.activeElement
} catch (e) {
return null
}
}
//create
function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
for (var i = start; i < end; i++) {
@ -143,8 +152,8 @@ module.exports = function($window) {
sentinel.$$reentrantLock$$ = true
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
}
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
initLifecycle(vnode.state, vnode, hooks)
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
sentinel.$$reentrantLock$$ = null
@ -246,7 +255,7 @@ module.exports = function($window) {
// When the list is being traversed top-down, at any index, the DOM nodes up to the previous
// vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old
// list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`.
//
//
// In the other scenarios (swaps, upwards traversal, map-based diff),
// the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the
// bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node
@ -502,8 +511,8 @@ module.exports = function($window) {
function updateComponent(parent, old, vnode, hooks, nextSibling, ns) {
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, 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.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
if (vnode.instance != null) {
if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns)
@ -623,15 +632,15 @@ module.exports = function($window) {
function removeNode(vnode) {
var expected = 1, called = 0
var original = vnode.state
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
var result = callHook.call(vnode.attrs.onbeforeremove, vnode)
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") {
var result = callHook.call(vnode.state.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
expected++
result.then(continuation, continuation)
}
}
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") {
var result = callHook.call(vnode.state.onbeforeremove, vnode)
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
var result = callHook.call(vnode.attrs.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
expected++
result.then(continuation, continuation)
@ -652,9 +661,9 @@ module.exports = function($window) {
}
}
function onremove(vnode) {
if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode)
if (typeof vnode.tag !== "string") {
if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
if (vnode.instance != null) onremove(vnode.instance)
} else {
var children = vnode.children
@ -683,7 +692,7 @@ module.exports = function($window) {
// Only do the coercion if we're actually going to check the value.
/* eslint-disable no-implicit-coercion */
//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && vnode.dom === $doc.activeElement) return
if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && vnode.dom === activeElement()) return
//setting select[value] to same value while having select open blinks select dropdown in Chrome
if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return
//setting option[value] to same value while having select open blinks select dropdown in Chrome
@ -708,7 +717,10 @@ module.exports = function($window) {
else if (
hasPropertyKey(vnode, key, ns)
&& key !== "className"
&& !(vnode.tag === "option" && key === "value")
&& !(key === "value" && (
vnode.tag === "option"
|| vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement()
))
&& !(vnode.tag === "input" && key === "type")
) {
vnode.dom[key] = null
@ -747,7 +759,7 @@ module.exports = function($window) {
}
}
function isFormAttribute(vnode, attr) {
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement() || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement
}
function isLifecycleMethod(attr) {
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
@ -764,26 +776,44 @@ module.exports = function($window) {
}
//style
var uppercaseRegex = /[A-Z]/g
function toLowerCase(capital) { return "-" + capital.toLowerCase() }
function normalizeKey(key) {
return key[0] === "-" && key[1] === "-" ? key :
key === "cssFloat" ? "float" :
key.replace(uppercaseRegex, toLowerCase)
}
function updateStyle(element, old, style) {
if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) {
if (old === style) {
// Styles are equivalent, do nothing.
} else if (style == null) {
// New style is missing, just clear it.
element.style.cssText = ""
} else if (typeof style !== "object") {
// New style is a string, let engine deal with patching.
element.style.cssText = style
} else if (old == null || typeof old !== "object") {
// `old` is missing or a string, `style` is an object.
element.style.cssText = ""
// Add new style properties
for (var key in style) {
var value = style[key]
if (value != null) element.style.setProperty(normalizeKey(key), String(value))
}
} else {
// Both old & new are (different) objects.
// Update style properties that have changed
for (var key in style) {
if (style[key] !== old[key]) element.style[key] = style[key]
var value = style[key]
if (value != null && (value = String(value)) !== String(old[key])) {
element.style.setProperty(normalizeKey(key), value)
}
}
// Remove style properties that no longer exist
for (var key in old) {
if (!(key in style)) element.style[key] = ""
}
return
}
if (old === style) element.style.cssText = "", old = null
if (style == null) element.style.cssText = ""
else if (typeof style === "string") element.style.cssText = style
else {
if (typeof old === "string") element.style.cssText = ""
for (var key in style) {
element.style[key] = style[key]
if (old[key] != null && style[key] == null) {
element.style.removeProperty(normalizeKey(key))
}
}
}
}
@ -804,9 +834,10 @@ module.exports = function($window) {
EventDict.prototype.handleEvent = function (ev) {
var handler = this["on" + ev.type]
var result
if (typeof handler === "function") result = handler.call(ev.target, ev)
if (typeof handler === "function") result = handler.call(ev.currentTarget, ev)
else if (typeof handler.handleEvent === "function") handler.handleEvent(ev)
if (typeof onevent === "function") onevent.call(ev.target, ev)
if (ev.redraw === false) ev.redraw = undefined
else if (typeof redraw === "function") redraw()
if (result === false) {
ev.preventDefault()
ev.stopPropagation()
@ -840,26 +871,27 @@ module.exports = function($window) {
if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode))
}
function shouldNotUpdate(vnode, old) {
var forceVnodeUpdate, forceComponentUpdate
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") {
forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
}
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") {
forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old)
}
if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
vnode.dom = old.dom
vnode.domSize = old.domSize
vnode.instance = old.instance
return true
}
return false
do {
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") {
var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
if (force !== undefined && !force) break
}
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") {
var force = callHook.call(vnode.state.onbeforeupdate, vnode, old)
if (force !== undefined && !force) break
}
return false
} while (false); // eslint-disable-line no-constant-condition
vnode.dom = old.dom
vnode.domSize = old.domSize
vnode.instance = old.instance
return true
}
function render(dom, vnodes) {
if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
var hooks = []
var active = $doc.activeElement
var active = activeElement()
var namespace = dom.namespaceURI
// First time rendering into a node clears it out
@ -868,10 +900,10 @@ module.exports = function($window) {
vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
dom.vnodes = vnodes
// document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
if (active != null && $doc.activeElement !== active && typeof active.focus === "function") active.focus()
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus()
for (var i = 0; i < hooks.length; i++) hooks[i]()
}
return {render: render, setEventCallback: setEventCallback}
return {render: render, setRedraw: setRedraw}
}

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="root"></div>
<script src="../../../mithril.js"></script>
<script>
var count = 0
var Button = {
view: function() {
return m(
"button",
{onclick: function() { count += 1 }},
"Inside the iframe: " + count)
}
}
m.mount(document.getElementById("root"), Button)
</script>
</body>
</html>

View file

@ -0,0 +1,9 @@
<html>
<body>
Various parent website content.
There should be a clickable button below, which is inside an iframe containing a mithril app:
<div>
<iframe src="./iframe.html"></iframe>
</div>
</body>
</html>

View file

@ -699,13 +699,23 @@ o.spec("component", function() {
"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)
})
if (hook === "onbeforeupdate") {
// the component's `onbeforeupdate` is called after the `attrs`' one
attrs[hook] = o.spy(function() {
o(attrs[hook].callCount).equals(methods[hook].callCount + 1)(hook)
})
methods[hook] = o.spy(function() {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
} else {
// the other component hooks are called before the `attrs` ones
methods[hook] = o.spy(function() {
o(attrs[hook].callCount).equals(methods[hook].callCount - 1)(hook)
})
attrs[hook] = o.spy(function() {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
}
})
var component = createComponent(methods)

View file

@ -33,6 +33,25 @@ o.spec("createElement", function() {
o(vnode.dom.nodeName).equals("DIV")
o(vnode.dom.style.backgroundColor).equals("red")
})
o("allows css vars in style", function() {
var vnode = {tag: "div", attrs: {style: {"--css-var": "red"}}}
render(root, [vnode])
o(vnode.dom.style["--css-var"]).equals("red")
})
o("allows css vars in style with uppercase letters", function() {
var vnode = {tag: "div", attrs: {style: {"--cssVar": "red"}}}
render(root, [vnode])
o(vnode.dom.style["--cssVar"]).equals("red")
})
o("censors cssFloat to float", function() {
var vnode = {tag: "a", attrs: {style: {cssFloat: "left"}}}
render(root, [vnode])
o(vnode.dom.style.float).equals("left")
})
o("creates children", function() {
var vnode = {tag: "div", children: [{tag: "a"}, {tag: "b"}]}
render(root, [vnode])

View file

@ -5,13 +5,13 @@ var domMock = require("../../test-utils/domMock")
var vdom = require("../../render/render")
o.spec("event", function() {
var $window, root, onevent, render
var $window, root, redraw, render
o.beforeEach(function() {
$window = domMock()
root = $window.document.body
onevent = o.spy()
redraw = o.spy()
var renderer = vdom($window)
renderer.setEventCallback(onevent)
renderer.setRedraw(redraw)
render = renderer.render
})
@ -28,10 +28,9 @@ o.spec("event", function() {
o(spy.this).equals(div.dom)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
@ -49,10 +48,9 @@ o.spec("event", function() {
o(spy.this).equals(div.dom)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(e.$defaultPrevented).equals(true)
o(e.$propagationStopped).equals(true)
})
@ -71,10 +69,9 @@ o.spec("event", function() {
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
@ -93,10 +90,30 @@ o.spec("event", function() {
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
o("handles propagated onclick", function() {
var spy = o.spy()
var child = {tag: "div"}
var parent = {tag: "div", attrs: {onclick: spy}, children: [child]}
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
render(root, [parent])
child.dom.dispatchEvent(e)
o(spy.callCount).equals(1)
o(spy.this).equals(parent.dom)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(child.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(e.$defaultPrevented).equals(false)
o(e.$propagationStopped).equals(false)
})
@ -254,10 +271,9 @@ o.spec("event", function() {
o(spy.this).equals(div.dom)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(div.dom).equals(updated.dom)
o(div.dom.attributes["id"].value).equals("b")
})
@ -278,10 +294,9 @@ o.spec("event", function() {
o(spy.this).equals(listener)
o(spy.args[0].type).equals("click")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
o(div.dom).equals(updated.dom)
o(div.dom.attributes["id"].value).equals("b")
})
@ -299,10 +314,9 @@ o.spec("event", function() {
o(spy.this).equals(div.dom)
o(spy.args[0].type).equals("transitionend")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("transitionend")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
})
o("handles transitionend EventListener object", function() {
@ -319,9 +333,8 @@ o.spec("event", function() {
o(spy.this).equals(listener)
o(spy.args[0].type).equals("transitionend")
o(spy.args[0].target).equals(div.dom)
o(onevent.callCount).equals(1)
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("transitionend")
o(onevent.args[0].target).equals(div.dom)
o(redraw.callCount).equals(1)
o(redraw.this).equals(undefined)
o(redraw.args.length).equals(0)
})
})

View file

@ -32,4 +32,166 @@ o.spec("fragment", function() {
o(frag.key).equals(7)
})
o.spec("children with no attrs", function() {
o("handles string single child", function() {
var vnode = fragment(["a"])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("a")
})
o("handles falsy string single child", function() {
var vnode = fragment([""])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
})
o("handles number single child", function() {
var vnode = fragment([1])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(1)
})
o("handles falsy number single child", function() {
var vnode = fragment([0])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0)
})
o("handles boolean single child", function() {
var vnode = fragment([true])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(true)
})
o("handles falsy boolean single child", function() {
var vnode = fragment([false])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
})
o("handles null single child", function() {
var vnode = fragment([null])
o(vnode.children[0]).equals(null)
})
o("handles undefined single child", function() {
var vnode = fragment([undefined])
o(vnode.children[0]).equals(undefined)
})
o("handles multiple string children", function() {
var vnode = fragment(["", "a"])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals("a")
})
o("handles multiple number children", function() {
var vnode = fragment([0, 1])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0)
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(1)
})
o("handles multiple boolean children", function() {
var vnode = fragment([false, true])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(true)
})
o("handles multiple null/undefined child", function() {
var vnode = fragment([null, undefined])
o(vnode.children[0]).equals(null)
o(vnode.children[1]).equals(undefined)
})
o("handles falsy number single child without attrs", function() {
var vnode = fragment(0)
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0)
})
})
o.spec("children with attrs", function() {
o("handles string single child", function() {
var vnode = fragment({}, ["a"])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("a")
})
o("handles falsy string single child", function() {
var vnode = fragment({}, [""])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
})
o("handles number single child", function() {
var vnode = fragment({}, [1])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(1)
})
o("handles falsy number single child", function() {
var vnode = fragment({}, [0])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0)
})
o("handles boolean single child", function() {
var vnode = fragment({}, [true])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(true)
})
o("handles falsy boolean single child", function() {
var vnode = fragment({}, [false])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
})
o("handles null single child", function() {
var vnode = fragment({}, [null])
o(vnode.children[0]).equals(null)
})
o("handles undefined single child", function() {
var vnode = fragment({}, [undefined])
o(vnode.children[0]).equals(undefined)
})
o("handles multiple string children", function() {
var vnode = fragment({}, ["", "a"])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals("a")
})
o("handles multiple number children", function() {
var vnode = fragment({}, [0, 1])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals(0)
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(1)
})
o("handles multiple boolean children", function() {
var vnode = fragment({}, [false, true])
o(vnode.children[0].tag).equals("#")
o(vnode.children[0].children).equals("")
o(vnode.children[1].tag).equals("#")
o(vnode.children[1].children).equals(true)
})
o("handles multiple null/undefined child", function() {
var vnode = fragment({}, [null, undefined])
o(vnode.children[0]).equals(null)
o(vnode.children[1]).equals(undefined)
})
})
})

View file

@ -358,6 +358,15 @@ o.spec("hyperscript", function() {
o(vnode.attrs.className).equals("a")
})
o("casts className using toString like browsers", function() {
const className = {
valueOf: () => ".valueOf",
toString: () => "toString"
}
var vnode = m("custom-element" + className, {className: className})
o(vnode.attrs.className).equals("valueOf toString")
})
})
o.spec("children", function() {
o("handles string single child", function() {

View file

@ -186,7 +186,7 @@ o.spec("onbeforeupdate", function() {
o(root.firstChild.attributes["id"].value).equals("b")
})
o("does not prevent update if returning false in component but true in vnode", function() {
o("prevents update if returning false in component but true in vnode", function() {
var component = createComponent({
onbeforeupdate: function() {return false},
view: function(vnode) {
@ -199,10 +199,10 @@ o.spec("onbeforeupdate", function() {
render(root, [vnode])
render(root, [updated])
o(root.firstChild.attributes["id"].value).equals("b")
o(root.firstChild.attributes["id"].value).equals("a")
})
o("does not prevent update if returning true in component but false in vnode", function() {
o("prevents update if returning true in component but false in vnode", function() {
var component = createComponent({
onbeforeupdate: function() {return true},
view: function(vnode) {
@ -215,7 +215,7 @@ o.spec("onbeforeupdate", function() {
render(root, [vnode])
render(root, [updated])
o(root.firstChild.attributes["id"].value).equals("b")
o(root.firstChild.attributes["id"].value).equals("a")
})
o("does not prevent update if returning true from component", function() {

View file

@ -179,19 +179,6 @@ o.spec("updateElement", function() {
o(updated.dom.style.backgroundColor).equals("red")
o(updated.dom.style.border).equals("")
})
o("updates style when it's same object but mutated", function() {
var style = {backgroundColor: "red", color: "gold"}
var vnode = {tag: "a", attrs: {style: style}}
render(root, [vnode])
delete style.backgroundColor
var updated = {tag: "a", attrs: {style: style}}
render(root, [updated])
o(updated.dom.style.backgroundColor).equals("")
o(updated.dom.style.color).equals("gold")
})
o("does not re-render element styles for equivalent style objects", function() {
var style = {color: "gold"}
var vnode = {tag: "a", attrs: {style: style}}

View file

@ -2,85 +2,108 @@
var buildQueryString = require("../querystring/build")
var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i")
module.exports = function($window, Promise) {
var callbackCount = 0
var oncompletion
function setCompletionCallback(callback) {oncompletion = callback}
function finalizer() {
var count = 0
function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()}
return function finalize(promise) {
var then = promise.then
promise.then = function() {
count++
var next = then.apply(promise, arguments)
next.then(complete, function(e) {
complete()
if (count === 0) throw e
})
return finalize(next)
function makeRequest(factory) {
return function(url, args) {
if (typeof url !== "string") { args = url; url = url.url }
else if (args == null) args = {}
var promise = new Promise(function(resolve, reject) {
factory(url, args, function (data) {
if (typeof args.type === "function") {
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
data[i] = new args.type(data[i])
}
}
else data = new args.type(data)
}
resolve(data)
}, reject)
})
if (args.background === true) return promise
var count = 0
function complete() {
if (--count === 0 && typeof oncompletion === "function") oncompletion()
}
return wrap(promise)
function wrap(promise) {
var then = promise.then
promise.then = function() {
count++
var next = then.apply(promise, arguments)
next.then(complete, function(e) {
complete()
if (count === 0) throw e
})
return wrap(next)
}
return promise
}
return promise
}
}
function normalize(args, extra) {
if (typeof args === "string") {
var url = args
args = extra || {}
if (args.url == null) args.url = url
function hasHeader(args, name) {
for (var key in args.headers) {
if ({}.hasOwnProperty.call(args.headers, key) && name.test(key)) return true
}
return args
return false
}
function request(args, extra) {
var finalize = finalizer()
args = normalize(args, extra)
function interpolate(url, data, assemble) {
if (data == null) return url
url = url.replace(/:([^\/]+)/gi, function (m, key) {
return data[key] != null ? data[key] : m
})
if (assemble && data != null) {
var querystring = buildQueryString(data)
if (querystring) url += (url.indexOf("?") < 0 ? "?" : "&") + querystring
}
return url
}
var promise = new Promise(function(resolve, reject) {
if (args.method == null) args.method = "GET"
args.method = args.method.toUpperCase()
return {
request: makeRequest(function(url, args, resolve, reject) {
var method = args.method != null ? args.method.toUpperCase() : "GET"
var useBody = method !== "GET" && method !== "TRACE" &&
(typeof args.useBody !== "boolean" || args.useBody)
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 data = args.data
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(data instanceof $window.FormData)
if (useBody) {
if (typeof args.serialize === "function") data = args.serialize(data)
else if (!(data instanceof $window.FormData)) data = JSON.stringify(data)
}
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)
xhr.open(method, interpolate(url, args.data, !useBody), typeof args.async !== "boolean" || args.async, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
if (args.serialize === JSON.stringify && useBody && !(args.headers && args.headers.hasOwnProperty("Content-Type"))) {
if (assumeJSON && useBody && !hasHeader(args, /^content-type$/i)) {
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
}
if (args.deserialize === deserialize && !(args.headers && args.headers.hasOwnProperty("Accept"))) {
if (typeof args.deserialize !== "function" && !hasHeader(args, /^accept$/i)) {
xhr.setRequestHeader("Accept", "application/json, text/*")
}
if (args.withCredentials) xhr.withCredentials = args.withCredentials
if (args.timeout) xhr.timeout = args.timeout
if (args.responseType) xhr.responseType = args.responseType
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
xhr.setRequestHeader(key, args.headers[key])
for (var key in args.headers) {
if ({}.hasOwnProperty.call(args.headers, key)) {
xhr.setRequestHeader(key, args.headers[key])
}
}
if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr
@ -91,10 +114,18 @@ module.exports = function($window, Promise) {
if (xhr.readyState === 4) {
try {
var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args))
if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
resolve(cast(args.type, response))
var success = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || (/^file:\/\//i).test(url)
var response = xhr.responseText
if (typeof args.extract === "function") {
response = args.extract(xhr, args)
success = true
} else if (typeof args.deserialize === "function") {
response = args.deserialize(response)
} else {
try {response = response ? JSON.parse(response) : null}
catch (e) {throw new Error("Invalid JSON: " + response)}
}
if (success) resolve(response)
else {
var error = new Error(xhr.responseText)
error.code = xhr.status
@ -108,22 +139,15 @@ module.exports = function($window, Promise) {
}
}
if (useBody && (args.data != null)) xhr.send(args.data)
if (useBody && data != null) xhr.send(data)
else xhr.send()
})
return args.background === true ? promise : finalize(promise)
}
function jsonp(args, extra) {
var finalize = finalizer()
args = normalize(args, extra)
var promise = new Promise(function(resolve, reject) {
}),
jsonp: makeRequest(function(url, args, resolve, reject) {
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
var script = $window.document.createElement("script")
$window[callbackName] = function(data) {
script.parentNode.removeChild(script)
resolve(cast(args.type, data))
resolve(data)
delete $window[callbackName]
}
script.onerror = function() {
@ -131,55 +155,14 @@ module.exports = function($window, Promise) {
reject(new Error("JSONP request failed"))
delete $window[callbackName]
}
if (args.data == null) args.data = {}
args.url = interpolate(args.url, args.data)
args.data[args.callbackKey || "callback"] = callbackName
script.src = assemble(args.url, args.data)
url = interpolate(url, args.data, true)
script.src = url + (url.indexOf("?") < 0 ? "?" : "&") +
encodeURIComponent(args.callbackKey || "callback") + "=" +
encodeURIComponent(callbackName)
$window.document.documentElement.appendChild(script)
})
return args.background === true? promise : finalize(promise)
}),
setCompletionCallback: function(callback) {
oncompletion = callback
},
}
function interpolate(url, data) {
if (data == null) return url
var tokens = url.match(/:[^\/]+/gi) || []
for (var i = 0; i < tokens.length; i++) {
var key = tokens[i].slice(1)
if (data[key] != null) {
url = url.replace(tokens[i], data[key])
}
}
return url
}
function assemble(url, data) {
var querystring = buildQueryString(data)
if (querystring !== "") {
var prefix = url.indexOf("?") < 0 ? "?" : "&"
url += prefix + querystring
}
return url
}
function deserialize(data) {
try {return data !== "" ? JSON.parse(data) : null}
catch (e) {throw new Error("Invalid JSON: " + data)}
}
function extract(xhr) {return xhr.responseText}
function cast(type, data) {
if (typeof type === "function") {
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
data[i] = new type(data[i])
}
}
else return new type(data)
}
return data
}
return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback}
}

View file

@ -415,9 +415,9 @@ o.spec("xhr", function() {
xhr({method: "GET", url: "/item", config: handleAbort}).catch(function() {
failed = true
})
.then(function() {
resolved = true
})
.then(function() {
resolved = true
})
})
o("doesn't fail on file:// status 0", function(done) {
mock.$defineRoutes({

View file

@ -1,6 +1,8 @@
# Change log for stream
## 2.0.0
- renamed HALT to SKIP [#2207](https://github.com/MithrilJS/mithril.js/pull/2207)
- rewrote implementation [#2207](https://github.com/MithrilJS/mithril.js/pull/2207)
- stream: Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150)
## 1.1.0

View file

@ -3,6 +3,7 @@
"version": "1.1.0",
"description": "Streaming data, mithril-style",
"main": "stream.js",
"module": "stream.mjs",
"directories": {
"test": "tests"
},

View file

@ -2,161 +2,156 @@
;(function() {
"use strict"
/* eslint-enable */
Stream.SKIP = {}
Stream.lift = lift
Stream.scan = scan
Stream.merge = merge
Stream.combine = combine
Stream.scanMerge = scanMerge
Stream["fantasy-land/of"] = Stream
var guid = 0, HALT = {}
function createStream() {
function stream() {
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0])
return stream._state.value
let warnedHalt = false
Object.defineProperty(Stream, "HALT", {
get: function() {
warnedHalt && console.log("HALT is deprecated and has been renamed to SKIP");
warnedHalt = true
return Stream.SKIP
}
initStream(stream)
})
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0])
function Stream(value) {
var dependentStreams = []
var dependentFns = []
function stream(v) {
if (arguments.length && v !== Stream.SKIP && open(stream)) {
value = v
stream.changing()
stream.state = "active"
dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
}
return value
}
stream.constructor = Stream
stream.state = arguments.length && value !== Stream.SKIP ? "active" : "pending"
stream.changing = function() {
open(stream) && (stream.state = "changing")
dependentStreams.forEach(function(s) {
s.dependent && s.dependent.changing()
s.changing()
})
}
stream.map = function(fn, ignoreInitial) {
var target = stream.state === "active" && ignoreInitial !== Stream.SKIP
? Stream(fn(value))
: Stream()
dependentStreams.push(target)
dependentFns.push(fn)
return target
}
let end
function createEnd() {
end = Stream()
end.map(function(value) {
if (value === true) {
stream.state = "ended"
dependentStreams.length = dependentFns.length = 0
}
return value
})
return end
}
stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value }
stream["fantasy-land/map"] = stream.map
stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) }
Object.defineProperty(stream, "end", {
get: function() { return end || createEnd() }
})
return stream
}
function initStream(stream) {
stream.constructor = createStream
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.toJSON = toJSON
Object.defineProperties(stream, {
end: {get: function() {
if (!stream._state.endStream) {
var endStream = createStream()
endStream.map(function(value) {
if (value === true) {
unregisterStream(stream)
endStream._state.unregister = function(){unregisterStream(endStream)}
}
return value
})
stream._state.endStream = endStream
}
return stream._state.endStream
}}
})
}
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) {
stream._state.value = value
stream._state.changed = true
if (stream._state.state !== 2) stream._state.state = 1
}
function updateDependency(stream, mustSync) {
var state = stream._state, parents = state.parents
if (parents.length > 0 && parents.every(active) && (mustSync || parents.some(changed))) {
var value = stream._state.derive()
if (value === HALT) return unregisterStream(stream)
updateState(stream, value)
}
}
function finalize(stream) {
stream._state.changed = false
for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false
}
function combine(fn, streams) {
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)]))
var ready = streams.every(function(s) {
if (s.constructor !== Stream)
throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream")
return s.state === "active"
})
var stream = ready
? Stream(fn.apply(null, streams.concat([streams])))
: Stream()
let changed = []
streams.forEach(function(s) {
s.map(function(value) {
changed.push(s)
if (ready || streams.every(function(s) { return s.state !== "pending" })) {
ready = true
stream(fn.apply(null, streams.concat([changed])))
changed = []
}
return value
}, Stream.SKIP).parent = stream
})
return stream
}
function initDependency(dep, streams, derive) {
var state = dep._state
state.derive = derive
state.parents = streams.filter(notEnded)
registerDependency(dep, state.parents)
updateDependency(dep, true)
return dep
}
function registerDependency(stream, parents) {
for (var i = 0; i < parents.length; i++) {
parents[i]._state.deps[stream._state.id] = stream
registerDependency(stream, parents[i]._state.parents)
}
}
function unregisterStream(stream) {
for (var i = 0; i < stream._state.parents.length; i++) {
var parent = stream._state.parents[i]
delete parent._state.deps[stream._state.id]
}
for (var id in stream._state.deps) {
var dependent = stream._state.deps[id]
var index = dependent._state.parents.indexOf(stream)
if (index > -1) dependent._state.parents.splice(index, 1)
}
stream._state.state = 2 //ended
stream._state.deps = {}
}
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])}
function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value}
function valid(stream) {return stream._state }
function active(stream) {return stream._state.state === 1}
function changed(stream) {return stream._state.changed}
function notEnded(stream) {return stream._state.state !== 2}
function merge(streams) {
return combine(function() {
return streams.map(function(s) {return s()})
}, streams)
return combine(function() { return streams.map(function(s) { return s() }) }, streams)
}
function scan(reducer, seed, stream) {
var newStream = combine(function (s) {
var next = reducer(seed, s._state.value)
if (next !== HALT) return seed = next
return HALT
}, [stream])
if (newStream._state.state === 0) newStream(seed)
return newStream
function scan(fn, acc, origin) {
var stream = origin.map(function(v) {
acc = fn(acc, v)
return acc
})
stream(acc)
return stream
}
function scanMerge(tuples, seed) {
var streams = tuples.map(function(tuple) {
var stream = tuple[0]
if (stream._state.state === 0) stream(undefined)
return stream
})
var streams = tuples.map(function(tuple) { return tuple[0] })
var newStream = combine(function() {
var stream = 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)
}
streams.forEach(function(stream, i) {
if (changed.indexOf(stream) > -1)
seed = tuples[i][1](seed, stream())
})
return seed
}, streams)
return newStream
stream(seed)
return stream
}
createStream["fantasy-land/of"] = createStream
createStream.merge = merge
createStream.combine = combine
createStream.scan = scan
createStream.scanMerge = scanMerge
createStream.HALT = HALT
function lift() {
var fn = arguments[0]
var streams = Array.prototype.slice.call(arguments, 1)
return merge(streams).map(function(streams) {
return fn.apply(undefined, streams)
})
}
if (typeof module !== "undefined") module["exports"] = createStream
else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = createStream
else window.m = {stream : createStream}
function open(s) {
return s.state === "pending" || s.state === "active" || s.state === "changing"
}
if (typeof module !== "undefined") module["exports"] = Stream
else if (typeof window.m === "function" && !("stream" in window.m)) window.m.stream = Stream
else window.m = {stream : Stream}
}());

151
stream/stream.mjs Normal file
View file

@ -0,0 +1,151 @@
/* eslint-enable */
Stream.SKIP = {}
Stream.lift = lift
Stream.scan = scan
Stream.merge = merge
Stream.combine = combine
Stream.scanMerge = scanMerge
Stream["fantasy-land/of"] = Stream
let warnedHalt = false
Object.defineProperty(Stream, "HALT", {
get: function() {
warnedHalt && console.log("HALT is deprecated and has been renamed to SKIP");
warnedHalt = true
return Stream.SKIP
}
})
function Stream(value) {
var dependentStreams = []
var dependentFns = []
function stream(v) {
if (arguments.length && v !== Stream.SKIP && open(stream)) {
value = v
stream.changing()
stream.state = "active"
dependentStreams.forEach(function(s, i) { s(dependentFns[i](value)) })
}
return value
}
stream.constructor = Stream
stream.state = arguments.length && value !== Stream.SKIP ? "active" : "pending"
stream.changing = function() {
open(stream) && (stream.state = "changing")
dependentStreams.forEach(function(s) {
s.dependent && s.dependent.changing()
s.changing()
})
}
stream.map = function(fn, ignoreInitial) {
var target = stream.state === "active" && ignoreInitial !== Stream.SKIP
? Stream(fn(value))
: Stream()
dependentStreams.push(target)
dependentFns.push(fn)
return target
}
let end
function createEnd() {
end = Stream()
end.map(function(value) {
if (value === true) {
stream.state = "ended"
dependentStreams.length = dependentFns.length = 0
}
return value
})
return end
}
stream.toJSON = function() { return value != null && typeof value.toJSON === "function" ? value.toJSON() : value }
stream["fantasy-land/map"] = stream.map
stream["fantasy-land/ap"] = function(x) { return combine(function(s1, s2) { return s1()(s2()) }, [x, stream]) }
Object.defineProperty(stream, "end", {
get: function() { return end || createEnd() }
})
return stream
}
function combine(fn, streams) {
var ready = streams.every(function(s) {
if (s.constructor !== Stream)
throw new Error("Ensure that each item passed to stream.combine/stream.merge/lift is a stream")
return s.state === "active"
})
var stream = ready
? Stream(fn.apply(null, streams.concat([streams])))
: Stream()
let changed = []
streams.forEach(function(s) {
s.map(function(value) {
changed.push(s)
if (ready || streams.every(function(s) { return s.state !== "pending" })) {
ready = true
stream(fn.apply(null, streams.concat([changed])))
changed = []
}
return value
}, Stream.SKIP).parent = stream
})
return stream
}
function merge(streams) {
return combine(function() { return streams.map(function(s) { return s() }) }, streams)
}
function scan(fn, acc, origin) {
var stream = origin.map(function(v) {
acc = fn(acc, v)
return acc
})
stream(acc)
return stream
}
function scanMerge(tuples, seed) {
var streams = tuples.map(function(tuple) { return tuple[0] })
var stream = combine(function() {
var changed = arguments[arguments.length - 1]
streams.forEach(function(stream, i) {
if (changed.indexOf(stream) > -1)
seed = tuples[i][1](seed, stream())
})
return seed
}, streams)
stream(seed)
return stream
}
function lift() {
var fn = arguments[0]
var streams = Array.prototype.slice.call(arguments, 1)
return merge(streams).map(function(streams) {
return fn.apply(undefined, streams)
})
}
function open(s) {
return s.state === "pending" || s.state === "active" || s.state === "changing"
}
export default Stream

View file

@ -31,7 +31,7 @@ o.spec("scan", function() {
o(result[3]).deepEquals({a: 1})
})
o("reducer can return HALT to prevent child updates", function() {
o("reducer can return SKIP to prevent child updates", function() {
var count = 0
var action = stream()
var store = stream.scan(function (arr, value) {
@ -39,7 +39,7 @@ o.spec("scan", function() {
case "number":
return arr.concat(value)
default:
return stream.HALT
return stream.SKIP
}
}, [], action)
var child = store.map(function (p) {

View file

@ -30,6 +30,43 @@ o.spec("stream", function() {
o(stream()()).equals(1)
})
o("can SKIP", function() {
var a = Stream(2)
var b = a.map(function(value) {
return value === 5
? Stream.SKIP
: value
})
a(5)
o(b()).equals(2)
})
o("can HALT", function() {
var a = Stream(2)
var b = a.map(function(value) {
return value === 5
? Stream.HALT
: value
})
a(5)
o(b()).equals(2)
})
o("warns HALT deprecated", function() {
var log = console.log
var warning = ""
console.log = function(a) {
warning = a
}
Stream.HALT
console.log = log
o(warning).equals("HALT is deprecated and has been renamed to SKIP")
})
})
o.spec("combine", function() {
o("transforms value", function() {
@ -87,6 +124,7 @@ o.spec("stream", function() {
o(d()).equals(15)
o(count).equals(1)
})
o("combines default value atomically", function() {
var count = 0
var a = Stream(3)
@ -100,6 +138,21 @@ o.spec("stream", function() {
o(d()).equals(15)
o(count).equals(1)
})
o("combines and maps nested streams atomically", function() {
var count = 0
var a = Stream(3)
var b = Stream.combine(function(a) {return a() * 2}, [a])
var c = Stream.combine(function(a) {return a() * a()}, [a])
var d = c.map(function(x){return x})
var e = Stream.combine(function(x) {return x()}, [d])
var f = Stream.combine(function(b, e) {
count++
return b() + e()
}, [b, e])
o(f()).equals(15)
o(count).equals(1)
})
o("combine lists only changed upstreams in last arg", function() {
var streams = []
var a = Stream()
@ -111,8 +164,22 @@ o.spec("stream", function() {
a(3)
b(5)
o(streams.length).equals(1)
o(streams[0]).equals(b)
o(streams.length).equals(2)
o(streams[0]).equals(a)
o(streams[1]).equals(b)
})
o("combine continues with ended streams", function() {
var a = Stream()
var b = Stream()
var combined = Stream.combine(function(a, b) {
return a() + b()
}, [a, b])
a(3)
a.end(true)
b(5)
o(combined()).equals(8)
})
o("combine lists only changed upstreams in last arg with default value", function() {
var streams = []
@ -151,11 +218,11 @@ o.spec("stream", function() {
o(b()()).equals(undefined)
})
o("combine can halt", function() {
o("combine can skip", function() {
var count = 0
var a = Stream(1)
var b = Stream.combine(function() {
return Stream.HALT
return Stream.SKIP
}, [a])["fantasy-land/map"](function() {
count++
return 1
@ -164,13 +231,13 @@ o.spec("stream", function() {
o(b()).equals(undefined)
o(count).equals(0)
})
o("combine can conditionaly halt", function() {
o("combine can conditionaly skip", function() {
var count = 0
var halt = false
var skip = false
var a = Stream(1)
var b = Stream.combine(function(a) {
if (halt) {
return Stream.HALT
if (skip) {
return Stream.SKIP
}
return a()
}, [a])["fantasy-land/map"](function(a) {
@ -179,7 +246,7 @@ o.spec("stream", function() {
})
o(b()).equals(1)
o(count).equals(1)
halt = true
skip = true
count = 0
a(2)
o(b()).equals(1)
@ -200,6 +267,127 @@ o.spec("stream", function() {
o(spy.callCount).equals(0)
})
})
o.spec("lift", function() {
o("transforms value", function() {
var stream = Stream()
var doubled = Stream.lift(function(s) {return s * 2}, stream)
stream(2)
o(doubled()).equals(4)
})
o("transforms default value", function() {
var stream = Stream(2)
var doubled = Stream.lift(function(s) {return s * 2}, stream)
o(doubled()).equals(4)
})
o("transforms multiple values", function() {
var s1 = Stream()
var s2 = Stream()
var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2)
s1(2)
s2(3)
o(added()).equals(5)
})
o("transforms multiple default values", function() {
var s1 = Stream(2)
var s2 = Stream(3)
var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2)
o(added()).equals(5)
})
o("transforms mixed default and late-bound values", function() {
var s1 = Stream(2)
var s2 = Stream()
var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2)
s2(3)
o(added()).equals(5)
})
o("lifts atomically", function() {
var count = 0
var a = Stream()
var b = Stream.lift(function(a) {return a * 2}, a)
var c = Stream.lift(function(a) {return a * a}, a)
var d = Stream.lift(function(b, c) {
count++
return b + c
}, b, c)
a(3)
o(d()).equals(15)
o(count).equals(1)
})
o("lifts default value atomically", function() {
var count = 0
var a = Stream(3)
var b = Stream.lift(function(a) {return a * 2}, a)
var c = Stream.lift(function(a) {return a * a}, a)
var d = Stream.lift(function(b, c) {
count++
return b + c
}, b, c)
o(d()).equals(15)
o(count).equals(1)
})
o("lift can return undefined", function() {
var a = Stream(1)
var b = Stream.lift(function() {
return undefined
}, a)
o(b()).equals(undefined)
})
o("lift can return stream", function() {
var a = Stream(1)
var b = Stream.lift(function() {
return Stream(2)
}, a)
o(b()()).equals(2)
})
o("lift can return pending stream", function() {
var a = Stream(1)
var b = Stream.lift(function() {
return Stream()
}, a)
o(b()()).equals(undefined)
})
o("lift can halt", function() {
var count = 0
var a = Stream(1)
var b = Stream.lift(function() {
return Stream.HALT
}, a)["fantasy-land/map"](function() {
count++
return 1
})
o(b()).equals(undefined)
o(count).equals(0)
})
o("lift will throw with a helpful error if given non-stream values", function () {
var spy = o.spy()
var a = Stream(1)
var thrown = null;
try {
Stream.lift(spy, a, "")
} catch (e) {
thrown = e
}
o(thrown).notEquals(null)
o(thrown.constructor === TypeError).equals(false)
o(spy.callCount).equals(0)
})
})
o.spec("merge", function() {
o("transforms an array of streams to an array of values", function() {
var all = Stream.merge([
@ -433,7 +621,7 @@ o.spec("stream", function() {
})
o.spec("applicative", function() {
o("identity", function() {
var a = Stream()["fantasy-land/of"](function(value) {return value})
var a = Stream["fantasy-land/of"](function(value) {return value})
var v = Stream(5)
o(v["fantasy-land/ap"](a)()).equals(5)
@ -444,16 +632,16 @@ o.spec("stream", function() {
var f = function(value) {return value * 2}
var x = 3
o(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))()).equals(6)
o(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))()).equals(a["fantasy-land/of"](f(x))())
o(a.constructor["fantasy-land/of"](x)["fantasy-land/ap"](a.constructor["fantasy-land/of"](f))()).equals(6)
o(a.constructor["fantasy-land/of"](x)["fantasy-land/ap"](a.constructor["fantasy-land/of"](f))()).equals(a.constructor["fantasy-land/of"](f(x))())
})
o("interchange", function() {
var u = Stream(function(value) {return value * 2})
var a = Stream()
var y = 3
o(a["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(6)
o(a["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(u["fantasy-land/ap"](a["fantasy-land/of"](function(f) {return f(y)}))())
o(a.constructor["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(6)
o(a.constructor["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(u["fantasy-land/ap"](a.constructor["fantasy-land/of"](function(f) {return f(y)}))())
})
})
})

View file

@ -220,6 +220,9 @@ module.exports = function(options) {
parseMarkup(value, root, [], "http://www.w3.org/2000/svg")
return {documentElement: root}
}
function camelCase(string) {
return string.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
}
var activeElement
var $window = {
DOMParser: DOMParser,
@ -227,29 +230,40 @@ module.exports = function(options) {
createElement: function(tag) {
var cssText = ""
var style = {}
Object.defineProperty(style, "cssText", {
get: function() {return cssText},
set: function (value) {
var buf = []
if (typeof value === "string") {
for (var key in style) style[key] = ""
var rules = splitDeclList(value)
for (var i = 0; i < rules.length; i++) {
var rule = rules[i]
var colonIndex = rule.indexOf(":")
if (colonIndex > -1) {
var rawKey = rule.slice(0, colonIndex).trim()
var key = rawKey.replace(/-\D/g, function(match) {return match[1].toUpperCase()})
var value = rule.slice(colonIndex + 1).trim()
if (key !== "cssText") {
style[key] = value
buf.push(rawKey + ": " + value + ";")
Object.defineProperties(style, {
cssText: {
get: function() {return cssText},
set: function (value) {
var buf = []
if (typeof value === "string") {
for (var key in style) style[key] = ""
var rules = splitDeclList(value)
for (var i = 0; i < rules.length; i++) {
var rule = rules[i]
var colonIndex = rule.indexOf(":")
if (colonIndex > -1) {
var rawKey = rule.slice(0, colonIndex).trim()
var key = camelCase(rawKey)
var value = rule.slice(colonIndex + 1).trim()
if (key !== "cssText") {
style[key] = style[rawKey] = value
buf.push(rawKey + ": " + value + ";")
}
}
}
element.setAttribute("style", cssText = buf.join(" "))
}
element.setAttribute("style", cssText = buf.join(" "))
}
}
},
getPropertyValue: {value: function(key){
return style[key]
}},
removeProperty: {value: function(key){
style[key] = style[camelCase(key)] = ""
}},
setProperty: {value: function(key, value){
style[key] = style[camelCase(key)] = value
}}
})
var events = {}
var element = {
@ -459,9 +473,9 @@ module.exports = function(options) {
if (!this.hasAttribute("type")) return "text"
var type = this.getAttribute("type")
return (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/)
.test(type)
? type
: "text"
.test(type)
? type
: "text"
},
set: typeSetter,
enumerable: true,

View file

@ -37,7 +37,7 @@ o.spec("test-utils/components", function() {
if (component.kind !== "constructible") {
o(cmp2).deepEquals(methods)
} else {
// deepEquals doesn't search the prototype, do it manually
// 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)

View file

@ -1430,22 +1430,22 @@ o.spec("domMock", function() {
o(input.type).equals("text")
})
"radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image"
.split("|").forEach(function(type) {
o("can be set to " + type, function(){
var input = $document.createElement("input")
input.type = type
.split("|").forEach(function(type) {
o("can be set to " + type, function(){
var input = $document.createElement("input")
input.type = type
o(input.getAttribute("type")).equals(type)
o(input.type).equals(type)
})
o("bad values set the attribute, but the getter corrects to 'text', " + type, function(){
var input = $document.createElement("input")
input.type = "badbad" + type
o(input.getAttribute("type")).equals(type)
o(input.type).equals(type)
})
o("bad values set the attribute, but the getter corrects to 'text', " + type, function(){
var input = $document.createElement("input")
input.type = "badbad" + type
o(input.getAttribute("type")).equals("badbad" + type)
o(input.type).equals("text")
o(input.getAttribute("type")).equals("badbad" + type)
o(input.type).equals("text")
})
})
})
})
o.spec("textarea[value]", function() {
o("reads from child if no value was ever set", function() {

View file

@ -11,7 +11,9 @@ module.exports = function() {
return {status: 500, responseText: "server error, most likely the URL was not defined " + url}
}
function FormData() {}
var $window = {
FormData: FormData,
XMLHttpRequest: function XMLHttpRequest() {
var args = {}
var headers = {}

View file

@ -24,7 +24,7 @@ o.spec("api", function() {
o("works", function() {
o(typeof m.version).equals("string")
o(m.version.indexOf(".") > -1).equals(true)
o(/\d/.test(m.version)).equals(true)
o((/\d/).test(m.version)).equals(true)
})
})
o.spec("m.trust", function() {
@ -45,16 +45,6 @@ o.spec("api", function() {
o(vnode.children[0].tag).equals("div")
})
})
o.spec("m.withAttr", 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")

View file

@ -1,9 +0,0 @@
"use strict"
module.exports = function (store) {
return {
get: function() { return store },
toJSON: function() { return store },
set: function(value) { return store = value }
}
}

View file

@ -1,17 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="../../module/module.js"></script>
<script src="../../ospec/ospec.js"></script>
<script src="../../util/withAttr.js"></script>
<script src="../../util/prop.js"></script>
<script src="test-withAttr.js"></script>
<script src="test-prop.js"></script>
<script>require("../../ospec/ospec").run()</script>
</body>
</html>

View file

@ -1,16 +0,0 @@
"use strict"
var o = require("../../ospec/ospec")
var prop = require("../../util/prop")
o.spec("prop", function() {
o("works", function() {
var p = prop(1)
o(p.get()).equals(1)
o(p.toJSON()).equals(1)
o(p.set(2)).equals(2)
o(p.get()).equals(2)
o(p.toJSON()).equals(2)
})
})

View file

@ -1,38 +0,0 @@
"use strict"
var o = require("../../ospec/ospec")
var withAttr = require("../../util/withAttr")
o.spec("withAttr", function() {
o("works", function() {
var spy = o.spy()
var context = {
handler: withAttr("value", spy)
}
context.handler({currentTarget: {value: 1}})
o(spy.args).deepEquals([1])
o(spy.this).equals(context)
})
o("works with attribute", function() {
var target = {
getAttribute: function() {return "readonly"}
}
var spy = o.spy()
var context = {
handler: withAttr("readonly", spy)
}
context.handler({currentTarget: target})
o(spy.args).deepEquals(["readonly"])
o(spy.this).equals(context)
})
o("context arg works", function() {
var spy = o.spy()
var context = {}
var handler = withAttr("value", spy, context)
handler({currentTarget: {value: 1}})
o(spy.this).equals(context)
})
})

View file

@ -1,7 +0,0 @@
"use strict"
module.exports = function(attrName, callback, context) {
return function(e) {
callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName))
}
}