Merge branch 'next'
This commit is contained in:
commit
aacfda739a
81 changed files with 6700 additions and 2285 deletions
|
|
@ -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
2
.gitattributes
vendored
|
|
@ -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
4
.github/CODEOWNERS
vendored
|
|
@ -1 +1,5 @@
|
|||
render/ @pygy
|
||||
render/ @isiahmeadows
|
||||
api/ @isiahmeadows
|
||||
performance/ @isiahmeadows
|
||||
util/ @isiahmeadows
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
25
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal 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 -->
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ mithril.js [](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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
42
docs/api.md
42
docs/api.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -183,4 +183,4 @@ traverseDirectory("./docs", function(pathname) {
|
|||
})
|
||||
}
|
||||
})
|
||||
.then(process.exit)
|
||||
.then(process.exit)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
152
docs/prop.md
152
docs/prop.md
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
136
docs/withAttr.md
136
docs/withAttr.md
|
|
@ -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
67
esm.js
Normal 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"
|
||||
)
|
||||
|
|
@ -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))),
|
||||
|
|
|
|||
9
index.js
9
index.js
|
|
@ -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
1038
mithril.js
File diff suppressed because it is too large
Load diff
2
mithril.min.js
vendored
2
mithril.min.js
vendored
File diff suppressed because one or more lines are too long
1
mithril.min.mjs
Normal file
1
mithril.min.mjs
Normal file
File diff suppressed because one or more lines are too long
1583
mithril.mjs
Normal file
1583
mithril.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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("/")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
19
ospec/esm.js
Normal 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)
|
||||
+ "()"
|
||||
)
|
||||
|
|
@ -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
363
ospec/ospec.mjs
Normal 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
|
||||
})
|
||||
()
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
2732
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
53
render/hyperscriptVnode.js
Normal file
53
render/hyperscriptVnode.js
Normal 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)
|
||||
}
|
||||
124
render/render.js
124
render/render.js
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
24
render/tests/manual/iframe.html
Normal file
24
render/tests/manual/iframe.html
Normal 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>
|
||||
9
render/tests/manual/index.html
Normal file
9
render/tests/manual/index.html
Normal 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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"version": "1.1.0",
|
||||
"description": "Streaming data, mithril-style",
|
||||
"main": "stream.js",
|
||||
"module": "stream.mjs",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
|
|
|
|||
253
stream/stream.js
253
stream/stream.js
|
|
@ -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
151
stream/stream.mjs
Normal 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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)}))())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue