clean up repo
This commit is contained in:
parent
e1d1691cb9
commit
1620b147da
32 changed files with 7 additions and 17724 deletions
|
|
@ -5,11 +5,6 @@ charset = utf-8
|
|||
|
||||
[*.js]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
<<<<<<< HEAD
|
||||
end_of_line = lf
|
||||
=======
|
||||
end_of_line = crlf
|
||||
>>>>>>> origin/next
|
||||
|
|
|
|||
93
.eslintrc
93
.eslintrc
|
|
@ -1,93 +0,0 @@
|
|||
{
|
||||
"extends": "eslint:recommended",
|
||||
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"indent": [2, "tab"],
|
||||
"no-trailing-spaces": 2,
|
||||
"linebreak-style": [2, "windows"],
|
||||
|
||||
"curly": [2, "multi-line"],
|
||||
"dot-location": [2, "property"],
|
||||
"dot-notation": [2, {"allowKeywords": false}],
|
||||
"eqeqeq": [2, "allow-null"],
|
||||
"no-alert": 2,
|
||||
"no-caller": 2,
|
||||
"guard-for-in": 2,
|
||||
"no-empty-label": 2,
|
||||
"no-eval": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-invalid-this": 2,
|
||||
"no-iterator": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-native-reassign": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-proto": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-throw-literal": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"no-useless-call": 2,
|
||||
"no-void": 2,
|
||||
"no-warning-comments": 1,
|
||||
"radix": 2,
|
||||
"wrap-iife": [2, "inside"],
|
||||
"strict": [2, "function"],
|
||||
"no-catch-shadow": 2,
|
||||
"no-label-var": 2,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-use-before-define": [2, "nofunc"],
|
||||
"array-bracket-spacing": 2,
|
||||
"block-spacing": 2,
|
||||
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
|
||||
"camelcase": 2,
|
||||
"comma-spacing": 2,
|
||||
"comma-style": 2,
|
||||
"consistent-this": [2, "self"],
|
||||
"eol-last": 2,
|
||||
"func-style": [2, "declaration"],
|
||||
"key-spacing": 2,
|
||||
"max-nested-callbacks": [2, 4],
|
||||
"new-parens": 2,
|
||||
"no-array-constructor": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-new-object": 2,
|
||||
"no-multiple-empty-lines": [2, {"max": 1}],
|
||||
"no-spaced-func": 2,
|
||||
"no-unneeded-ternary": 2,
|
||||
"object-curly-spacing": 2,
|
||||
"one-var": [2, {"initialized": "never"}],
|
||||
"operator-assignment": 2,
|
||||
"operator-linebreak": [2, "after"],
|
||||
"padded-blocks": [2, "never"],
|
||||
"quote-props": [2, "as-needed", {"keywords": true}],
|
||||
"quotes": [2, "double", "avoid-escape"],
|
||||
"semi-spacing": 0,
|
||||
"semi": [2, "never"],
|
||||
"space-after-keywords": 2,
|
||||
"space-before-function-paren": [2, {
|
||||
"anonymous": "always",
|
||||
"named": "never"
|
||||
}],
|
||||
"space-before-keywords": 2,
|
||||
"space-in-parens": 2,
|
||||
"space-infix-ops": [2, {"int32Hint": true}],
|
||||
"space-unary-ops": 2,
|
||||
"spaced-comment": [2, "always"],
|
||||
"max-depth": [2, 4],
|
||||
"max-len": [2, 80, 4],
|
||||
"max-statements": [2, 20]
|
||||
}
|
||||
}
|
||||
8
.gitattributes
vendored
8
.gitattributes
vendored
|
|
@ -1,7 +1 @@
|
|||
<<<<<<< HEAD
|
||||
* text=auto
|
||||
=======
|
||||
* text eol=crlf
|
||||
*.min.js binary
|
||||
*.map binary
|
||||
>>>>>>> origin/next
|
||||
* text=auto
|
||||
834
CONTRIBUTING.md
834
CONTRIBUTING.md
|
|
@ -1,834 +0,0 @@
|
|||
# Bug reports
|
||||
|
||||
Use the [issue tracker](https://github.com/lhorie/mithril.js/issues). Do check to make sure your bug hasn't already been filed. Please give the following information, where possible:
|
||||
|
||||
1. The version of Mithril you're using, whether it's the dev version or [the version on npm](http://npm.im/mithril.js). The version on npm may not have all the latest bug fixes, so your bug might very well be fixed in the dev version.
|
||||
2. The name and version of the browser(s) affected.
|
||||
3. A detailed explanation of the bug.
|
||||
4. A test case. The simpler, the better.
|
||||
|
||||
# Feature requests
|
||||
|
||||
Use the [issue tracker](https://github.com/lhorie/mithril.js/issues). Please do the following, where possible:
|
||||
|
||||
1. Check to make sure your suggestion hasn't already been filed. There's a nice collection of some of these feature requests [here](https://github.com/lhorie/mithril.js/issues/802).
|
||||
2. Clearly denote your issue as a feature request. It helps make the intent clearer. Even better is to denote it in the title.
|
||||
3. Describe your idea. This is the most important part.
|
||||
4. Submit, and wait for feedback. Don't forget to drop by the [Gitter room](https://gitter.im/lhorie/mithril.js) to get some publicity. It gets traffic daily. :smile:
|
||||
|
||||
# Contributing
|
||||
|
||||
We welcome any and all contributions. This is a community-driven project. Although we don't have a lot, we do have a few guidelines for contributions.
|
||||
|
||||
1. Please try to adhere to the style guide. Most of it is checked by ESLint. ESLint is also set up to check for other common errors, such as undeclared variables and invalid `typeof` values.
|
||||
2. Please make sure there are no regressions with your patch. Please don't disable existing tests, and please don't send a PR with new, disabled tests.
|
||||
3. For any new features introduced, be sure to write new unit tests for it. Maximum coverage is what we want.
|
||||
4. Try to not leave any extra `TODO`s, `FIXME`s, etc. in your code. ESLint will nag at you until you fix whatever problem it is.
|
||||
- Note that it's only a warning, not an error. It won't fail the CI tests, and there's a few outstanding ones inside Mithril right now.
|
||||
- If you must, use a `TODO(<your_username>): whatever` (or the equivalent `FIXME`, etc.) if it's something you are actively working on, or if it's something more general, file an issue titled "TODO: <short_description>" and reference the `TODO` comment itself.
|
||||
|
||||
It is assumed that for all contributions you make, you have the appropriate rights to and it may be made available under the MIT License, the license used for this project.
|
||||
|
||||
# Style Guide
|
||||
|
||||
The style is checked with ESLint. This style guide is here for one reason only: consistency. This should work for most code here, but it shouldn't be considered absolute &endash; consistency with the surrounding code is higher priority than following this guide. Also, if you need some sort of hack that doesn't follow this style guide like [Bluebird's `toFastProperties` hack](https://stackoverflow.com/q/24987896), make sure the code is consistent with what's around it, and disable whatever ESLint warnings you need to. (In that case, you would use `no-eval` and `no-unused-expressions`).
|
||||
|
||||
### EditorConfig
|
||||
|
||||
This project has its own [EditorConfig](http://editorconfig.org/). Most common editors either support it natively or have a plugin to support it. Here's links for several editors:
|
||||
|
||||
- [Atom](https://atom.io/packages/editorconfig)
|
||||
- [Nodepad++](https://github.com/editorconfig/editorconfig-notepad-plus-plus#readme)
|
||||
- [Sublime Text](https://packagecontrol.io/packages/EditorConfig)
|
||||
- [Vim](http://www.vim.org/scripts/script.php?script_id=3934)
|
||||
- [Emacs](https://marmalade-repo.org/packages/editorconfig)
|
||||
- IntelliJ, WebStorm: You're already set, since it supports EditorConfig natively.
|
||||
- [Eclipse](https://marketplace.eclipse.org/content/editorconfig-eclipse)
|
||||
- [Visual Studio](https://visualstudiogallery.msdn.microsoft.com/c8bccfe2-650c-4b42-bc5c-845e21f96328)
|
||||
- [Xcode](https://github.com/MarcoSero/EditorConfig-Xcode)
|
||||
- [Komodo](http://komodoide.com/packages/addons/editorconfig/)
|
||||
|
||||
EditorConfig also has [their own curated list](http://editorconfig.org/#download) of plugins. Do note that Text Mate's plugin doesn't support multiple properties used in this repo's `.editorconfig`.
|
||||
|
||||
### Line length
|
||||
|
||||
Please keep line length down to a maximum of 80 characters.
|
||||
|
||||
The only exception to this rule is with long regexes and test names. You can use `// eslint-disable-line max-len` to suppress this.
|
||||
|
||||
### Function length
|
||||
|
||||
Please keep function length to no more than 20 statements, including those in nested blocks.
|
||||
|
||||
The only exceptions are for revealing module wrappers and Mocha `describe` and `context` blocks. If you need to suppress this, you can use `// eslint-disable-line max-statements` at the beginning of the function.
|
||||
|
||||
This isn't checked for the tests, but still, please keep it reasonable.
|
||||
|
||||
### Line endings
|
||||
|
||||
Use Windows-style line endings (i.e. CRLF).
|
||||
|
||||
### Semicolons
|
||||
|
||||
Avoid semicolons. A few resources to help you understand this: [*](http://blog.izs.me/post/2353458699/an-open-letter-to-javascript-leaders-regarding) [*](http://inimino.org/~inimino/blog/javascript_semicolons) [*](http://jamesallardice.com/understanding-automatic-semi-colon-insertion-in-javascript/)
|
||||
|
||||
### Redundant expressions
|
||||
|
||||
Don't use redundant parentheses, curly braces, `undefined`s, ternaries, etc.
|
||||
|
||||
- `var foo = value === 1 ? true : false` is better written as `var foo = value === 1`.
|
||||
- `var foo = undefined` is better written as `var foo`.
|
||||
- `return undefined` is better written as `return`.
|
||||
- `delete(obj.foo)` is better written as `delete obj.foo`.
|
||||
- As an exception, use an extra set of parentheses when assigning in conditions.
|
||||
|
||||
### Equality testing
|
||||
|
||||
Prefer strict equality (`===`/`!==`) to loose equality (`==`/`!=`), unless you're comparing against `null` or `undefined`, where loose equality (`== null`/`!= null`) is preferred. It's more type-safe with primitives.
|
||||
|
||||
### `eval` and friends
|
||||
|
||||
Never use `eval`, the `Function` constructor, and friends. Those will fail when running under CSP restrictions, and this library should work in those environments.
|
||||
|
||||
### Quotes
|
||||
|
||||
Prefer double quotes to single quotes, but using single quotes to avoid escaping is okay.
|
||||
|
||||
### Strict mode
|
||||
|
||||
Any code not in the global scope should be in strict mode unless there's a good reason it shouldn't be. Browsers run it faster, and there's certain things where it's safer.
|
||||
|
||||
## Comments
|
||||
|
||||
Comments are helpful. Use descriptive comments where needed, so others can read it and understand it. If a non-obvious hack is used, explain it with a comment. But don't repeat yourself with a redundant comment when the code adequately describes itself.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var CARD_DECK_SIZE = 52
|
||||
|
||||
// Shuffle the deck of cards.
|
||||
for (var i = 0; i < CARD_DECK_SIZE; i++) {
|
||||
var j = i + (Math.random() * CARD_DECK_SIZE - i)|0
|
||||
|
||||
var tmp = deck[i]
|
||||
deck[i] = deck[j]
|
||||
deck[j] = tmp
|
||||
}
|
||||
|
||||
// Also good
|
||||
function toFastProperties(obj) {
|
||||
// Bluebird's toFastProperties hack. Forces V8 to optimize object as prototype,
|
||||
// significantly speeding up property access.
|
||||
|
||||
/* eslint-disable no-eval */
|
||||
function C() {}
|
||||
C.prototype = obj
|
||||
new C()
|
||||
return
|
||||
eval(obj)
|
||||
/* eslint-enable no-eval */
|
||||
}
|
||||
|
||||
// Bad
|
||||
var CARD_DECK_SIZE = 52
|
||||
|
||||
// Shuffle the deck of cards.
|
||||
function shuffle(deck) {
|
||||
for (var i = 0; i < CARD_DECK_SIZE; i++) {
|
||||
// Generate a random card index to swap at.
|
||||
var j = i + (Math.random() * CARD_DECK_SIZE - i)|0
|
||||
|
||||
// Swap the cards at each index.
|
||||
var tmp = deck[i]
|
||||
deck[i] = deck[j]
|
||||
deck[j] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
// Also bad
|
||||
// This function loops through each item in a list, calling `f` with each item
|
||||
// and their respective index. If the function `f` returns explicitly with
|
||||
// `false`, iteration stops. This function returns the original list for
|
||||
// convenience.
|
||||
function forEach(list, f) {
|
||||
// Loop through each entry in the list with the loop index `i`
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
// Call the function with the current item and index
|
||||
if (f(list[i], i) === false) {
|
||||
// If the function explicitly returns `false`, immediately stop
|
||||
// iteration.
|
||||
break
|
||||
}
|
||||
}
|
||||
// Return the list, for convenience.
|
||||
return list
|
||||
}
|
||||
```
|
||||
|
||||
### Magic values
|
||||
|
||||
Prefer constant variables to magic values where it helps with code readability. Also, don't use comments in place of a constant.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var CARD_DECK_SIZE = 52
|
||||
for (var i = 0; i < CARD_DECK_SIZE; i++) {
|
||||
var j = i + (Math.random() * CARD_DECK_SIZE - i)|0
|
||||
var tmp = a[i]
|
||||
a[i] = a[j]
|
||||
a[j] = tmp
|
||||
}
|
||||
|
||||
// Bad
|
||||
for (var i = 0; i < 52; i++) {
|
||||
var j = i + (Math.random() * 52 - i)|0
|
||||
var tmp = a[i]
|
||||
a[i] = a[j]
|
||||
a[j] = tmp
|
||||
}
|
||||
|
||||
// Also bad
|
||||
// Note: 52 cards in a deck
|
||||
for (var i = 0; i < 52; i++) {
|
||||
var j = i + (Math.random() * 52 - i)|0
|
||||
var tmp = a[i]
|
||||
a[i] = a[j]
|
||||
a[j] = tmp
|
||||
}
|
||||
```
|
||||
|
||||
### Initial whitespace
|
||||
|
||||
Start your comments with a space. It's more readable.
|
||||
|
||||
```js
|
||||
// Good
|
||||
// This is a comment.
|
||||
|
||||
// Bad
|
||||
//This is a comment.
|
||||
```
|
||||
|
||||
### Grammar and spelling
|
||||
|
||||
Please try to use proper grammar and spelling. Not all of us are perfect English speakers (even us native ones struggle at times), but it's easier to understand down the road when reading the code.
|
||||
|
||||
## Whitespace
|
||||
|
||||
Use vertical whitespace and indentation to your advantage. It helps with readability, and minifiers are astonishingly great at removing this. :stuck_out_tongue_winking_eye:
|
||||
|
||||
```js
|
||||
// Good
|
||||
function iterate() {
|
||||
var index = 0
|
||||
var list = []
|
||||
while (hasNext()) {
|
||||
list.push(next(index))
|
||||
index++
|
||||
}
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
read(list[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Bad
|
||||
function iterate() {
|
||||
var index = 0
|
||||
var list = []
|
||||
while (hasNext()) list.push(next(index++))
|
||||
for (var i = 0; i < list.length; i++) read(list[i])
|
||||
}
|
||||
|
||||
// Also bad
|
||||
function iterate() {
|
||||
var list = []
|
||||
for (var index = 0; hasNext(); list.push(next(index++))) {}
|
||||
for (var i = 0; i < list.length; i++) read(list[i])
|
||||
}
|
||||
```
|
||||
|
||||
### Trailing whitespace
|
||||
|
||||
Please don't leave trailing spaces, even in blank likes. It makes diffs harder to read. If your [editor supports EditorConfig, or you've downloaded a plugin for it](http://editorconfig.org/#download), you're already set. Otherwise, searching "<editor> strip trailing whitespace blank lines" should help you.
|
||||
|
||||
### Indentation and vertical whitespace
|
||||
|
||||
Indent with hard tabs. Each one counts for 4 spaces.
|
||||
|
||||
Never mix tabs and spaces. Don't use smart tabs.
|
||||
|
||||
### Excessive whitespace
|
||||
|
||||
Keep whitespace within reason. Limit vertical whitespace to no more than 1 consecutive blank line, and don't start or end blocks with plain whitespace. Don't use more than one character of horizontal whitespace beyond indentation.
|
||||
|
||||
```js
|
||||
// Bad
|
||||
function iterate() {
|
||||
var index = 0
|
||||
var list = []
|
||||
|
||||
while (hasNext()) {
|
||||
list.push(next(index))
|
||||
index++
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
read(list[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Also bad
|
||||
var a = 2
|
||||
// ^^ ^^
|
||||
```
|
||||
|
||||
If you're find yourself resorting to multiple consecutive blank lines to separate logic, consider refactoring the code into smaller functions if possible.
|
||||
|
||||
### Control keywords
|
||||
|
||||
Always surround control keywords (e.g. `if`, `else`, `for`) with whitespace.
|
||||
|
||||
## Operators
|
||||
|
||||
### Binary operators
|
||||
|
||||
Always surround binary keyword operators (e.g. `in`, `instanceof`) with whitespace.
|
||||
|
||||
Always surround any other binary operator with whitespace, including assignment operators. Add line breaks after the operator, not before.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var a = 1 + 2
|
||||
a = 3
|
||||
|
||||
var a = 1 + 2 + 3 + 4 + 5 +
|
||||
6 + 7 + 8 + 9 + 10
|
||||
|
||||
// Bad
|
||||
var a = 1+2
|
||||
var a=1+2
|
||||
a=3
|
||||
|
||||
var a = 1 + 2 + 3 + 4 + 5
|
||||
+ 6 + 7 + 8 + 9 + 10
|
||||
```
|
||||
|
||||
In the event you're casting to a 32-bit integer (i.e. `x|0`), it's okay to omit the whitespace. Mithril doesn't currently have any instances of this, but it may in the future.
|
||||
|
||||
```js
|
||||
// This is okay
|
||||
var casted = value|0
|
||||
```
|
||||
|
||||
### Unary operators
|
||||
|
||||
Always use whitespace between an unary keyword operator (e.g. `delete`, `new`) with whitespace.
|
||||
|
||||
Do not use spaces between any other unary operator and the value they apply to.
|
||||
|
||||
```js
|
||||
// Good
|
||||
!a
|
||||
-1
|
||||
~1
|
||||
|
||||
// Bad
|
||||
! a
|
||||
- 1
|
||||
~ 1
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
### Exterior whitespace
|
||||
|
||||
Do not include space between the opening curly brace and the first object key in single line objects. For multi-line objects, don't include any extra preceding whitespace at the beginning or end.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var object = {foo: 1}
|
||||
|
||||
var object = {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
quux: "string"
|
||||
}
|
||||
|
||||
// Bad
|
||||
var object = { foo: 1 }
|
||||
|
||||
var object = {
|
||||
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
quux: "string"
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Interior whitespace
|
||||
|
||||
Use no space before an object key and the colon, but use a single space between the colon and value.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var object = {foo: 1}
|
||||
|
||||
// Bad
|
||||
var object = {foo:1}
|
||||
var object = {foo : 1}
|
||||
var object = {foo :1}
|
||||
```
|
||||
|
||||
### Larger objects
|
||||
|
||||
Non-trivial objects should occupy multiple lines.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var object = {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
quux: "string"
|
||||
}
|
||||
|
||||
// Bad
|
||||
var object = {foo: 1, bar: 2, baz: 3, quux: "string"}
|
||||
```
|
||||
|
||||
Prefer plain objects over `new function () {}`, but if you feel the latter is better, use `// eslint-disable-line no-new-func` on the first line.
|
||||
|
||||
### Comma placement
|
||||
|
||||
Commas come last. Also, don't use trailing commas.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var object = {
|
||||
foo: 1,
|
||||
bar: 2
|
||||
}
|
||||
|
||||
// Bad
|
||||
var object = {
|
||||
foo: 1,
|
||||
bar: 2, // Trailing comma
|
||||
}
|
||||
|
||||
var object = {
|
||||
foo: 1
|
||||
, bar: 2 // Comma first
|
||||
}
|
||||
|
||||
// Also bad
|
||||
var object = {foo: 1,}
|
||||
```
|
||||
|
||||
### Member access
|
||||
|
||||
When chaining methods and/or properties across multiple lines, dots come first.
|
||||
|
||||
```js
|
||||
// Good
|
||||
object
|
||||
.foo()
|
||||
.bar()
|
||||
|
||||
// Bad
|
||||
object.
|
||||
foo().
|
||||
bar()
|
||||
```
|
||||
|
||||
### Property quoting
|
||||
|
||||
Quote properties when needed. Never quote valid identifiers. This may lead to some inconsistency in whether properties are quoted or not in the object, but that inconsistency is okay.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var object = {
|
||||
foo: 1,
|
||||
bar: 2
|
||||
}
|
||||
|
||||
var object = {
|
||||
foo: 1,
|
||||
"non-identifier": 2
|
||||
}
|
||||
|
||||
// Bad
|
||||
var object = {
|
||||
"foo": 1,
|
||||
"bar": 2
|
||||
}
|
||||
|
||||
var object = {
|
||||
"foo": 1,
|
||||
"non-identifier": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Iteration
|
||||
|
||||
When iterating objects with `for-in`, filter it with `Object.prototype.hasOwnProperty` first. This may be on the same line if that's the only condition. If this is used more than once, make a local `hasOwn` alias to use like `hasOwn.call(object, prop)`.
|
||||
|
||||
## Variables and declarations
|
||||
|
||||
Use camel case (e.g. `loopIndex`) for variables, pascal case (e.g. `MyClass`) for classes, and upper case with underscores (e.g. `MAX_VALUE`) for constants.
|
||||
|
||||
Use `self` to capture `this` where needed. (i.e. `var self = this`)
|
||||
|
||||
Prefer short but clear, descriptive, and self-documenting variable names.
|
||||
|
||||
Single letter names are generally okay in these contexts:
|
||||
|
||||
- Loop variables: `i`, `j`, etc.
|
||||
- Function arguments in small, trivial functional utilities: `f`, `g`, etc.
|
||||
- Standard mathematical algorithms: `x`, `y`, `a`, `b`, etc.
|
||||
|
||||
```js
|
||||
// Good
|
||||
function forEach(list, f) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
f(list[i], i)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Also good
|
||||
function forEach(list, func) {
|
||||
for (var index = 0; index < list.length; index++) {
|
||||
func(list[index], index)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Bad
|
||||
function iterateArray(listOfEntries, callback) {
|
||||
for (var index = 0; index < listOfEntries.length; index++) {
|
||||
callback(listOfEntries[index], index)
|
||||
}
|
||||
return listOfEntries
|
||||
}
|
||||
|
||||
// Also bad
|
||||
function e(l, c) {
|
||||
for (var x = 0; x < l.length; x++) {
|
||||
c(l[x], x)
|
||||
}
|
||||
return l
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple variable declarations
|
||||
|
||||
If a variable is assigned a value when it's declared, it gets its own line.
|
||||
|
||||
If a variable is not immediately assigned a value, it may be grouped with others that aren't first assigned values.
|
||||
|
||||
Do group related variables.
|
||||
|
||||
Variable declarations in the init block of a for loop are excluded from this rule, but the number of declarations should still be minimized.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var a = 1
|
||||
var b = 2
|
||||
var c, d
|
||||
|
||||
// Also good
|
||||
var foo, bar, baz, quux
|
||||
var spam, eggs, ham
|
||||
var shouldUpdate, initialize
|
||||
|
||||
// Okay, since it's within a for loop
|
||||
for (var i = 0, test; (test = foo === 1); i++) {
|
||||
doSomething(i)
|
||||
}
|
||||
|
||||
// Bad
|
||||
var a = 1, b = 2, c, d
|
||||
|
||||
// Also bad
|
||||
var foo, bar, baz, quux, spam, eggs, ham, shouldUpdate, initialize
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
### Native functions
|
||||
|
||||
Don't assign to native functions or prototypes beyond polyfills. Ever.
|
||||
|
||||
### Function declarations
|
||||
|
||||
Don't assign to a function declaration. Declarations look like static values, so they should be treated that way. There is no difference in size, either.
|
||||
|
||||
```js
|
||||
// Bad
|
||||
function foo() { return 1 }
|
||||
foo = function () { return 2 }
|
||||
|
||||
// Also bad
|
||||
function foo() { return 1 }
|
||||
foo = 2
|
||||
```
|
||||
|
||||
### Conditions
|
||||
|
||||
Avoid assigning directly in conditions. If you need to assign in a condition, wrap them in a new set of parentheses.
|
||||
|
||||
```js
|
||||
// Good
|
||||
var test = foo === 1
|
||||
if (test) {
|
||||
doSomething()
|
||||
}
|
||||
|
||||
var test = foo === 1
|
||||
for (var i = 0; test; i++) {
|
||||
doSomething(i)
|
||||
test = foo === 1
|
||||
}
|
||||
|
||||
if ((test = foo === 1)) {
|
||||
doSomething()
|
||||
}
|
||||
|
||||
for (var i = 0, test; (test = foo === 1); i++) {
|
||||
doSomething(i)
|
||||
}
|
||||
|
||||
// Bad
|
||||
if (test = foo === 1) {
|
||||
doSomething()
|
||||
}
|
||||
|
||||
for (var i = 0, test; test = foo === 1; i++) {
|
||||
doSomething(i)
|
||||
}
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
Prefer anonymous functions to named functions in expressions where possible
|
||||
|
||||
Prefer named function declarations to assigning a function to a declared variable where possible.
|
||||
|
||||
```js
|
||||
// Good
|
||||
setTimeout(function () { doSomething() }, 0)
|
||||
|
||||
function foo() {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Good, uses recursion
|
||||
setTimeout(function f() {
|
||||
doSomething()
|
||||
if (shouldRepeat()) setTimeout(f, 0)
|
||||
}, 0)
|
||||
|
||||
// Bad
|
||||
requestAnimationFrame(function foo() { runNext() })
|
||||
|
||||
var foo = function () {
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
### Anonymous functions
|
||||
|
||||
Anonymous functions must have a single space between `function` and the arguments and between the arguments and the opening brace.
|
||||
|
||||
### Named functions, function declarations
|
||||
|
||||
Named functions and function declarations must have no space between the function name and the arguments and a single space between the arguments and the opening brace.
|
||||
|
||||
## Blocks and conditionals
|
||||
|
||||
### Curly braces
|
||||
|
||||
Curly braces are required for blocks if the body takes up more than one line. This goes for functions as well. They are optional if it fits within one line, but don't be afraid to use them where you see fit.
|
||||
|
||||
Do put an extra space after the first brace and before the final brace in single-line functions.
|
||||
|
||||
```js
|
||||
// Good
|
||||
if (condition) doSomething()
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
} else {
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
setTimeout(function () { doSomething() }, 0)
|
||||
|
||||
setTimeout(function () {
|
||||
doSomething()
|
||||
}, 0)
|
||||
|
||||
// Bad
|
||||
if (condition) { doSomething(); doSomethingElse() }
|
||||
|
||||
if (condition)
|
||||
doSomething()
|
||||
|
||||
setTimeout(function () { doSomething(); doSomethingElse() }, 0)
|
||||
|
||||
setTimeout(function () {doSomething()}, 0)
|
||||
```
|
||||
|
||||
### Function declarations
|
||||
|
||||
Function declarations should always take more than one line.
|
||||
|
||||
```js
|
||||
// Good
|
||||
function foo() {
|
||||
return bar
|
||||
}
|
||||
|
||||
// Bad
|
||||
function foo() { return bar }
|
||||
```
|
||||
|
||||
### `if`-`else`
|
||||
|
||||
Be consistent with braces in `if`-`else` statements.
|
||||
|
||||
```js
|
||||
// Good
|
||||
if (condition) doSomething()
|
||||
else doSomethingElse()
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
} else {
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
} else if (anotherCondition) {
|
||||
doSomethingElse()
|
||||
} else {
|
||||
doAnotherThing()
|
||||
}
|
||||
|
||||
// Okay
|
||||
if (condition) doSomething()
|
||||
else if (anotherCondition) doSomethingElse()
|
||||
else doAnotherThing()
|
||||
|
||||
// Bad
|
||||
if (condition) doSomething()
|
||||
else {
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
if (condition) doSomething()
|
||||
else if (anotherCondition) {
|
||||
doSomethingElse()
|
||||
} else doAnotherThing()
|
||||
|
||||
|
||||
if (condition) {
|
||||
doSomething()
|
||||
} else doSomethingElse()
|
||||
```
|
||||
|
||||
## `do`-`while` loops
|
||||
|
||||
Always use braces.
|
||||
|
||||
```js
|
||||
// Good
|
||||
do {
|
||||
doSomething()
|
||||
} while (condition)
|
||||
|
||||
// Bad
|
||||
do doSomething()
|
||||
while (condition)
|
||||
```
|
||||
|
||||
### Empty blocks
|
||||
|
||||
Mark empty blocks as intentionally empty via a comment or similar. Don't just leave an empty block and/or semicolon.
|
||||
|
||||
```js
|
||||
// Good
|
||||
for (var i = 0; i < list.length && cond(list[i]); i++) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Even better
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (!cond(list[i])) break
|
||||
}
|
||||
|
||||
// Bad
|
||||
for (var i = 0; i < list.length && cond(list[i]); i++) {}
|
||||
|
||||
// Also bad
|
||||
for (var i = 0; i < list.length && cond(list[i]); i++) {
|
||||
;
|
||||
}
|
||||
|
||||
// Even worse
|
||||
for (var i = 0; i < list.length && cond(list[i]); i++);
|
||||
```
|
||||
|
||||
### Nesting
|
||||
|
||||
Don't nest `if` statements in `else` blocks if you don't need to.
|
||||
|
||||
```js
|
||||
// Good
|
||||
if (test) {
|
||||
doSomething()
|
||||
} else if (otherTest) {
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
if (test) {
|
||||
doSomething()
|
||||
} else {
|
||||
if (otherTest) {
|
||||
doSomethingElse()
|
||||
}
|
||||
|
||||
doThisOtherThing()
|
||||
}
|
||||
|
||||
// Bad
|
||||
if (test) {
|
||||
doSomething()
|
||||
} else {
|
||||
if (otherTest) {
|
||||
doSomethingElse()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Don't nest curly braces beyond 4 levels deep. This includes blocks, loops, and functions, as well as IIFE wrappers. This also includes the tests. There's rarely a reason to go farther, as most of the time, it's a signal to refactor and/or re-organize your code.
|
||||
|
||||
### Brace style
|
||||
|
||||
Put the `else`, `catch`, and `finally` on the same line as its closing brace.
|
||||
387
Gruntfile.js
387
Gruntfile.js
|
|
@ -1,387 +0,0 @@
|
|||
/* eslint-env node */
|
||||
module.exports = function (grunt) { // eslint-disable-line
|
||||
|
||||
var pkg = grunt.file.readJSON("package.json")
|
||||
var currentYear = grunt.template.today("yyyy")
|
||||
var inputFolder = "./docs"
|
||||
var tempFolder = "./temp"
|
||||
var archiveFolder = "./archive"
|
||||
var outputFolder = "../mithril"
|
||||
|
||||
var guide = [
|
||||
"auto-redrawing",
|
||||
"benchmarks",
|
||||
"community",
|
||||
"optimizing-performance",
|
||||
"comparison",
|
||||
"components",
|
||||
"getting-started",
|
||||
"installation",
|
||||
"integration",
|
||||
"practices",
|
||||
"refactoring",
|
||||
"routing",
|
||||
"tools",
|
||||
"web-services"
|
||||
]
|
||||
|
||||
var api = [
|
||||
"change-log",
|
||||
"roadmap",
|
||||
"how-to-read-signatures",
|
||||
"mithril",
|
||||
"mithril.computation",
|
||||
"mithril.deferred",
|
||||
"mithril.mount",
|
||||
"mithril.component",
|
||||
"mithril.prop",
|
||||
"mithril.redraw",
|
||||
"mithril.render",
|
||||
"mithril.deps",
|
||||
"mithril.request",
|
||||
"mithril.route",
|
||||
"mithril.sync",
|
||||
"mithril.trust",
|
||||
"mithril.withAttr",
|
||||
"mithril.xhr"
|
||||
]
|
||||
|
||||
var md2htmlTasks = {}
|
||||
function makeTasks(layout, pages) {
|
||||
pages.forEach(function (name) {
|
||||
var src = inputFolder + "/" + name + ".md"
|
||||
var title = ""
|
||||
|
||||
if (grunt.file.exists(src)) {
|
||||
title = grunt.file.read(src).split(/\n/)[0].substring(3) +
|
||||
" - "
|
||||
}
|
||||
|
||||
md2htmlTasks[name] = {
|
||||
options: {
|
||||
layout: inputFolder + "/layout/" + layout + ".html",
|
||||
templateData: {topic: title}
|
||||
},
|
||||
files: [{
|
||||
src: [src],
|
||||
dest: tempFolder + "/" + name + ".html"
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
makeTasks("guide", guide)
|
||||
makeTasks("api", api)
|
||||
|
||||
var currentVersionArchiveFolder = archiveFolder + "/v" + pkg.version
|
||||
|
||||
grunt.initConfig({
|
||||
// Keep this in sync with the .eslintignore
|
||||
eslint: {
|
||||
options: {
|
||||
extensions: [".js"],
|
||||
fix: true
|
||||
},
|
||||
all: [
|
||||
"**/*.js",
|
||||
"!node_modules/**",
|
||||
"!**/*.min.js",
|
||||
"!archive/**",
|
||||
"!deploy/**",
|
||||
"!mithril.closure-compiler-externs.js",
|
||||
"!docs/layout/lib/**"
|
||||
]
|
||||
},
|
||||
|
||||
mocha_phantomjs: { // eslint-disable-line camelcase
|
||||
test: {
|
||||
src: ["test/index.html", "tests/index.html"],
|
||||
options: {
|
||||
reporter: "dot"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
md2html: md2htmlTasks,
|
||||
|
||||
uglify: {
|
||||
options: {
|
||||
banner: [
|
||||
"/*",
|
||||
"Mithril v" + pkg.version,
|
||||
pkg.homepage,
|
||||
"(c) 2014-" + currentYear + " " + pkg.author.name,
|
||||
"License: " + pkg.license,
|
||||
"*/"
|
||||
].join("\n"),
|
||||
sourceMap: true
|
||||
},
|
||||
mithril: {src: "mithril.js", dest: "mithril.min.js"}
|
||||
},
|
||||
|
||||
zip: {
|
||||
distribution: {
|
||||
cwd: currentVersionArchiveFolder + "/",
|
||||
src: [
|
||||
currentVersionArchiveFolder + "/mithril.min.js",
|
||||
currentVersionArchiveFolder + "/mithril.min.js.map",
|
||||
currentVersionArchiveFolder + "/mithril.js"
|
||||
],
|
||||
dest: currentVersionArchiveFolder + "/mithril.min.zip"
|
||||
}
|
||||
},
|
||||
|
||||
replace: {
|
||||
options: {
|
||||
force: true,
|
||||
patterns: [
|
||||
{match: /\.md/g, replacement: ".html"},
|
||||
{match: /\$version/g, replacement: pkg.version}
|
||||
]
|
||||
},
|
||||
|
||||
links: {
|
||||
expand: true,
|
||||
flatten: true,
|
||||
src: [tempFolder + "/**/*.html"],
|
||||
dest: currentVersionArchiveFolder + "/"
|
||||
},
|
||||
|
||||
index: {
|
||||
src: inputFolder + "/layout/index.html",
|
||||
dest: currentVersionArchiveFolder + "/index.html"
|
||||
},
|
||||
|
||||
commonjs: {
|
||||
expand: true,
|
||||
flatten: true,
|
||||
src: [inputFolder + "/layout/*.json"],
|
||||
dest: currentVersionArchiveFolder
|
||||
},
|
||||
|
||||
cdnjs: {
|
||||
src: "deploy/cdnjs-package.json",
|
||||
dest: "../cdnjs/ajax/libs/mithril/package.json"
|
||||
}
|
||||
},
|
||||
|
||||
copy: {
|
||||
style: {
|
||||
src: inputFolder + "/layout/style.css",
|
||||
dest: currentVersionArchiveFolder + "/style.css"
|
||||
},
|
||||
|
||||
pages: {
|
||||
src: inputFolder + "/layout/pages.json",
|
||||
dest: currentVersionArchiveFolder + "/pages.json"
|
||||
},
|
||||
|
||||
lib: {
|
||||
expand: true,
|
||||
cwd: inputFolder + "/layout/lib/",
|
||||
src: "./**",
|
||||
dest: currentVersionArchiveFolder + "/lib/"
|
||||
},
|
||||
|
||||
tools: {
|
||||
expand: true,
|
||||
cwd: inputFolder + "/layout/tools/",
|
||||
src: "./**",
|
||||
dest: currentVersionArchiveFolder + "/tools/"
|
||||
},
|
||||
|
||||
comparisons: {
|
||||
expand: true,
|
||||
cwd: inputFolder + "/layout/comparisons/",
|
||||
src: "./**",
|
||||
dest: currentVersionArchiveFolder + "/comparisons/"
|
||||
},
|
||||
|
||||
unminified: {
|
||||
src: "mithril.js",
|
||||
dest: currentVersionArchiveFolder + "/mithril.js"
|
||||
},
|
||||
|
||||
minified: {
|
||||
src: "mithril.min.js",
|
||||
dest: currentVersionArchiveFolder + "/mithril.min.js"
|
||||
},
|
||||
|
||||
readme: {
|
||||
src: "README.md",
|
||||
dest: currentVersionArchiveFolder + "/README.md"
|
||||
},
|
||||
|
||||
map: {
|
||||
src: "mithril.min.js.map",
|
||||
dest: currentVersionArchiveFolder + "/mithril.min.js.map"
|
||||
},
|
||||
|
||||
typescript: {
|
||||
src: "mithril.d.ts",
|
||||
dest: currentVersionArchiveFolder + "/mithril.d.ts"
|
||||
},
|
||||
|
||||
publish: {
|
||||
expand: true,
|
||||
cwd: currentVersionArchiveFolder,
|
||||
src: "./**",
|
||||
dest: outputFolder
|
||||
},
|
||||
|
||||
archive: {
|
||||
expand: true,
|
||||
cwd: currentVersionArchiveFolder,
|
||||
src: "./**",
|
||||
dest: outputFolder + "/archive/v" + pkg.version
|
||||
}
|
||||
},
|
||||
|
||||
"saucelabs-browsers": {
|
||||
firefox: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
if (browser.browserName !== "firefox") return false
|
||||
var version = browser.version
|
||||
return version === "dev" || version === "beta" ||
|
||||
+version >= 38 // The latest ESR version
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
chrome: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
if (browser.browserName !== "chrome") return false
|
||||
var version = browser.version
|
||||
return version === "dev" || version === "beta" ||
|
||||
+version >= 41
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
ie: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
return browser.browserName === "internet explorer" &&
|
||||
!/2003/.test(browser.platform)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
edge: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
return browser.browserName === "microsoftedge"
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
safari: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
return browser.browserName === "safari"
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
opera: {
|
||||
filter: function (browsers) {
|
||||
return browsers.filter(function (browser) {
|
||||
return browser.browserName === "opera"
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
saucelabs: {
|
||||
all: {
|
||||
options: {
|
||||
username: process.env.SAUCE_USERNAME,
|
||||
key: process.env.SAUCE_ACCESS_KEY,
|
||||
testname: "Mithril Tests " + new Date().toJSON(),
|
||||
browsers: "<%= saucelabs.browsers %>",
|
||||
urls: ["http://localhost:8000/test/index.html"],
|
||||
sauceConfig: {
|
||||
"record-video": false,
|
||||
"record-screenshots": false
|
||||
},
|
||||
build: process.env.TRAVIS_JOB_ID,
|
||||
onTestComplete: function (result, callback) {
|
||||
var user = process.env.SAUCE_USERNAME
|
||||
var pass = process.env.SAUCE_ACCESS_KEY
|
||||
|
||||
var url = [
|
||||
"https://saucelabs.com/rest/v1", user, "jobs",
|
||||
result.job_id
|
||||
].join("/")
|
||||
|
||||
require("request").put({
|
||||
url: url,
|
||||
auth: {user: user, pass: pass},
|
||||
json: {passed: result.passed}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
} else if (response.statusCode !== 200) {
|
||||
return callback(new Error(
|
||||
"Unexpected response status: " +
|
||||
response.statusCode + "\n "))
|
||||
} else {
|
||||
return callback(null, result.passed)
|
||||
}
|
||||
})
|
||||
},
|
||||
tunnelTimeout: 5
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
connect: {
|
||||
server: {
|
||||
options: {
|
||||
port: 8888,
|
||||
base: "."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clean: {
|
||||
options: {force: true},
|
||||
generated: [tempFolder]
|
||||
}
|
||||
})
|
||||
|
||||
grunt.loadNpmTasks("grunt-saucelabs-browsers")
|
||||
grunt.loadNpmTasks("grunt-contrib-clean")
|
||||
grunt.loadNpmTasks("grunt-contrib-copy")
|
||||
grunt.loadNpmTasks("grunt-contrib-uglify")
|
||||
grunt.loadNpmTasks("grunt-md2html")
|
||||
grunt.loadNpmTasks("grunt-replace")
|
||||
grunt.loadNpmTasks("grunt-zip")
|
||||
grunt.loadNpmTasks("grunt-contrib-connect")
|
||||
grunt.loadNpmTasks("grunt-saucelabs")
|
||||
grunt.loadNpmTasks("grunt-eslint")
|
||||
grunt.loadNpmTasks("grunt-mocha-phantomjs")
|
||||
|
||||
grunt.registerTask("build", [
|
||||
// "lint",
|
||||
"test",
|
||||
"uglify",
|
||||
"zip",
|
||||
"md2html",
|
||||
"replace",
|
||||
"copy",
|
||||
"clean"
|
||||
])
|
||||
|
||||
grunt.registerTask("lint", ["eslint:all"])
|
||||
grunt.registerTask("test", ["mocha_phantomjs"])
|
||||
grunt.registerTask("default", ["build"])
|
||||
|
||||
grunt.registerTask("sauce", [
|
||||
"saucelabs-browsers:all",
|
||||
"connect",
|
||||
"saucelabs"
|
||||
])
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
### Description:
|
||||
|
||||
[Describe bug here]
|
||||
|
||||
### Steps to Reproduce:
|
||||
|
||||
```
|
||||
[Provide a small snippet of code where the incorrect behavior can be seen]
|
||||
```
|
||||
|
||||
### Expected:
|
||||
|
||||
[What was the expected behavior]
|
||||
|
||||
### Actual:
|
||||
|
||||
[What actually happened]
|
||||
|
||||
|
|
@ -15,6 +15,12 @@
|
|||
Mithril is a modern client-side Javascript framework for building Single Page Applications.
|
||||
It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box.
|
||||
|
||||
<style>
|
||||
@keyframes grow {
|
||||
from {transform:scaleX(0)}
|
||||
to {transform:scaleX(100%)}
|
||||
}
|
||||
</style>
|
||||
<div style="display:flex;margin:0 0 30px;">
|
||||
<div style="width:50%;">
|
||||
<h5>Download size</h5>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
8
archive/v0.2.4/mithril.min.js
vendored
8
archive/v0.2.4/mithril.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,31 +1,7 @@
|
|||
# Change log
|
||||
|
||||
<<<<<<< HEAD
|
||||
- [Migrating from v0.2.x](#migrating-from-v02x)
|
||||
- [Older docs](http://mithril.js.org/archive/v0.2.5/change-log.html)
|
||||
=======
|
||||
[v0.2.5](http://mithril.js.org/archive/v0.2.5)
|
||||
|
||||
### News:
|
||||
|
||||
- performance improvements in IE (thanks to @gyandeeps)
|
||||
- m.request now has a `callbackName` option to specify the name of the javascript function that gets called on JSONP response [#1072](https://github.com/lhorie/mithril.js/issues/1072)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
- fix active form element syncing [#691](https://github.com/lhorie/mithril.js/issues/691)
|
||||
- ignore url interpolations without value in m.request [#1039](https://github.com/lhorie/mithril.js/issues/1039)
|
||||
- fixed native promise absorption in `m.prop` [#1076](https://github.com/lhorie/mithril.js/issues/1076)
|
||||
|
||||
---
|
||||
|
||||
[v0.2.4](http://mithril.js.org/archive/v0.2.4)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
- fix regression that caused errors to be swallowed in promises returned by m.request [#968](https://github.com/lhorie/mithril.js/issues/968)
|
||||
- fix ReferenceError when calling an event handler via mithril-query without an event argument
|
||||
>>>>>>> origin/next
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -306,7 +306,6 @@ var Header = {
|
|||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
m(Header, [
|
||||
m("h1", "My title"),
|
||||
m("h2", "Lorem ipsum"),
|
||||
|
|
@ -321,25 +320,6 @@ m(Header, [
|
|||
m("h2", "Lorem ipsum"),
|
||||
])
|
||||
```
|
||||
=======
|
||||
//ContactForm no longer calls `Contact.save`
|
||||
var ContactForm = {
|
||||
controller: function(args) {
|
||||
var ctrl = this
|
||||
ctrl.contact = m.prop(new Contact())
|
||||
ctrl.save = function(contact) {
|
||||
Observable.trigger("saveContact", {contact: contact})
|
||||
ctrl.contact = m.prop(new Contact()) //reset to empty contact
|
||||
}
|
||||
return ctrl
|
||||
},
|
||||
view: function(ctrl, args) {
|
||||
var contact = ctrl.contact()
|
||||
|
||||
return m("form", [
|
||||
m("label", "Name"),
|
||||
m("input", {oninput: m.withAttr("value", contact.name), value: contact.name()}),
|
||||
>>>>>>> origin/next
|
||||
|
||||
The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,617 +0,0 @@
|
|||
## Getting Started
|
||||
|
||||
### What is Mithril?
|
||||
|
||||
Mithril is a client-side Javascript MVC framework, i.e. it's a tool to make application code divided into a data layer (called **M**odel), a UI layer (called **V**iew), and a glue layer (called **C**ontroller)
|
||||
|
||||
Mithril is around 7.8 kB gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization.
|
||||
|
||||
The goal of the framework is to make application code discoverable, readable and maintainable, and hopefully help you become an even better developer.
|
||||
|
||||
Unlike some frameworks, Mithril tries very hard to avoid locking you into a web of dependencies: you can use as *little* of the framework as you need.
|
||||
|
||||
However, using its entire toolset idiomatically can bring lots of benefits: learning to use functional programming in real world scenarios and solidifying good coding practices for OOP and MVC are just some of them.
|
||||
|
||||
---
|
||||
|
||||
## A Simple Application
|
||||
|
||||
Once you have a [copy of Mithril](installation.md), getting started is surprisingly boilerplate-free:
|
||||
|
||||
```markup
|
||||
<!doctype html>
|
||||
<title>Todo app</title>
|
||||
<script src="mithril.min.js"></script>
|
||||
<body>
|
||||
<script>
|
||||
//app goes here
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
Yes, this is valid HTML 5! According to the specs, the `<html>` and `<head>` tags can be omitted, but their respective DOM elements will still be there implicitly when a browser renders that markup.
|
||||
|
||||
---
|
||||
|
||||
### Model
|
||||
|
||||
In Mithril, an application typically lives in a namespace and contains components. Components are merely structures that represent a viewable "page" or a part of a page. In addition, an application can be organizationally divided into three major layers: Model, Controller and View.
|
||||
|
||||
For simplicity, our application will have only one component, and we're going to use it as the namespace for our application.
|
||||
|
||||
In Mithril, a *component* is an object that contains a `view` function and optionally a `controller` function.
|
||||
|
||||
```
|
||||
//an empty Mithril component
|
||||
var myComponent = {
|
||||
controller: function() {},
|
||||
view: function() {}
|
||||
}
|
||||
```
|
||||
|
||||
In addition to holding a controller and a view, a component can also be used to store data that pertains to it.
|
||||
|
||||
Let's create a component.
|
||||
|
||||
```markup
|
||||
<script>
|
||||
//this application only has one component: todo
|
||||
var todo = {};
|
||||
</script>
|
||||
```
|
||||
|
||||
Typically, model entities are reusable and live outside of components (e.g. `var User = ...`). In our example, since the whole application lives in one component, we're going to use the component as a namespace for our model entities.
|
||||
|
||||
```javascript
|
||||
var todo = {};
|
||||
|
||||
//for simplicity, we use this component to namespace the model classes
|
||||
|
||||
//the Todo class has two properties
|
||||
todo.Todo = function(data) {
|
||||
this.description = m.prop(data.description);
|
||||
this.done = m.prop(false);
|
||||
};
|
||||
|
||||
//the TodoList class is a list of Todo's
|
||||
todo.TodoList = Array;
|
||||
```
|
||||
|
||||
[`m.prop`](mithril.prop.md) is simply a factory for a getter-setter function. Getter-setters work like this:
|
||||
|
||||
```javascript
|
||||
//define a getter-setter with initial value `John`
|
||||
var a_name = m.prop("John");
|
||||
|
||||
//read the value
|
||||
var a = a_name(); //a == "John"
|
||||
|
||||
//set the value to `Mary`
|
||||
a_name("Mary"); //Mary
|
||||
|
||||
//read the value
|
||||
var b = a_name(); //b == "Mary"
|
||||
```
|
||||
|
||||
Note that the `Todo` and `TodoList` classes we defined above are plain vanilla Javascript constructors. They can be initialized and used like this:
|
||||
|
||||
```javascript
|
||||
var myTask = new todo.Todo({description: "Write code"});
|
||||
|
||||
//read the description
|
||||
myTask.description(); //Write code
|
||||
|
||||
//is it done?
|
||||
var isDone = myTask.done(); //isDone == false
|
||||
|
||||
//mark as done
|
||||
myTask.done(true); //true
|
||||
|
||||
//now it's done
|
||||
isDone = myTask.done(); //isDone == true
|
||||
```
|
||||
|
||||
The `TodoList` class is simply an alias of the native `Array` class.
|
||||
|
||||
```javascript
|
||||
var list = new todo.TodoList();
|
||||
list.length; //0
|
||||
```
|
||||
|
||||
According to the classic definition of the MVC pattern, the model layer is responsible for data storage, state management and business logic.
|
||||
|
||||
You can see that our classes above fit the criteria: they have all the methods and properties that they need to be assembled into a meaningful state. A `Todo` can be instantiated, and have its properties changed. The list can have todo items added to it via the `push` method. And so on.
|
||||
|
||||
#### View-Model
|
||||
|
||||
Our next step is to write a view-model that will use our model classes. A view-model is a model level entity that stores UI state. In many frameworks UI state is typically stored in a controller, but doing so makes the code harder to scale since controllers aren't designed to be data providers. In Mithril, UI state is understood to be model data, even though it doesn't necessarily map to a database ORM entity.
|
||||
|
||||
View-models are also responsible for handling business logic that revolves around UI-specific restrictions. For example a form might have an input and a cancel button. In such a case, it's the view-model's responsibility to track the current state of the input vs the original state and to apply a cancellation, if required. In the event the form was saved, then view-model would delegate saving to a more appropriate ORM model entity.
|
||||
|
||||
In the case of our todo application, the view-model needs a few things: it needs to track a running list of todos and a field for adding new todos, and it needs to handle the logic of adding to the todo and the implications of this action of the UI.
|
||||
|
||||
```javascript
|
||||
//define the view-model
|
||||
todo.vm = {
|
||||
init: function() {
|
||||
//a running list of todos
|
||||
todo.vm.list = new todo.TodoList();
|
||||
|
||||
//a slot to store the name of a new todo before it is created
|
||||
todo.vm.description = m.prop('');
|
||||
|
||||
//adds a todo to the list, and clears the description field for user convenience
|
||||
todo.vm.add = function(description) {
|
||||
if (description()) {
|
||||
todo.vm.list.push(new todo.Todo({description: description()}));
|
||||
todo.vm.description("");
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The code above defines a view-model object called `vm`. It is simply a javascript object that has an `init` function. This function initializes the `vm` object with three members: `list`, which is simply an array, `description`, which is an `m.prop` getter-setter function with an empty string as the initial value, and `add`, which is a method that adds a new Todo instance to `list` if an input description getter-setter is not an empty string.
|
||||
|
||||
Later in this guide, we'll pass the `description` property as the parameter to this function. When we get there, I'll explain why we're passing description as an argument instead of simply using OOP-style member association.
|
||||
|
||||
You can use the view-model like this:
|
||||
|
||||
```javascript
|
||||
//initialize our view-model
|
||||
todo.vm.init();
|
||||
|
||||
todo.vm.description(); //[empty string]
|
||||
|
||||
//try adding a to-do
|
||||
todo.vm.add(todo.vm.description);
|
||||
todo.vm.list.length; //0, because you can't add a to-do with an empty description
|
||||
|
||||
//add it properly
|
||||
todo.vm.description("Write code");
|
||||
todo.vm.add(todo.vm.description);
|
||||
todo.vm.list.length; //1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Controller
|
||||
|
||||
In classic MVC, the role of the controller is to dispatch actions from the view to the model layer. In traditional server-side frameworks, the controller layer is of large significance because the nature of HTTP requests, responses and the framework abstractions that are exposed to developers require that the controller act as an adapter layer to transform the serialized data from HTTP requests to something that can be passed to ORM model methods.
|
||||
|
||||
In client-side MVC, however, this dissonance doesn't exist, and controllers can be extremely simple. Mithril controllers can be stripped down to a bare minimum, so that they only perform a single essential role: to expose a scoped set of model-level functionality. As you may recall, models are responsible for encapsulating business logic, and view-models encapsulate logic that pertains specifically to UI state, so there's really nothing else for a controller to abstract away, and all it needs to do is expose a slice of the model layer that pertains to the UI that is currently in view.
|
||||
|
||||
In other words, all our controller needs to do is this:
|
||||
|
||||
```javascript
|
||||
todo.controller = function() {
|
||||
todo.vm.init()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### View
|
||||
|
||||
The next step is to write a view so users can interact with the application. In Mithril, views are plain Javascript. This comes with several benefits (proper error reporting, proper lexical scoping, etc.), while still allowing [HTML syntax to be used via a preprocessor tool](https://github.com/insin/msx)
|
||||
|
||||
```javascript
|
||||
todo.view = function() {
|
||||
return m("html", [
|
||||
m("body", [
|
||||
m("input"),
|
||||
m("button", "Add"),
|
||||
m("table", [
|
||||
m("tr", [
|
||||
m("td", [
|
||||
m("input[type=checkbox]")
|
||||
]),
|
||||
m("td", "task description"),
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
The utility method `m()` creates virtual DOM elements. As you can see, you can use CSS selectors to specify attributes. You can also use the `.` syntax to add CSS classes and the `#` to add an id.
|
||||
|
||||
In fact, when not using the [MSX](https://github.com/insin/msx) HTML syntax preprocessor, it's recommended that you embrace using CSS selectors (e.g. `m(".modal-body")`) to really benefit from their inherent semantic expressiveness.
|
||||
|
||||
For the purposes of testing out our code so far, the view can be rendered using the `m.render` method:
|
||||
|
||||
```javascript
|
||||
m.render(document, todo.view());
|
||||
```
|
||||
|
||||
Notice that we pass a root DOM element to attach our template to, as well as the template itself.
|
||||
|
||||
This renders the following markup:
|
||||
|
||||
```markup
|
||||
<html>
|
||||
<body>
|
||||
<input />
|
||||
<button>Add</button>
|
||||
<table>
|
||||
<tr>
|
||||
<td><input type="checkbox" /></td>
|
||||
<td>task description</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that `m.render` is a very low level method in Mithril that draws only once and doesn't attempt to run the auto-redrawing system. In order to enable auto-redrawing, the `todo` component must be initialized by either calling `m.mount` or by creating a route definition with `m.route`. Also note that, unlike observable-based frameworks like Knockout.js, setting a value in a `m.prop` getter-setter does NOT trigger redrawing side-effects in Mithril.
|
||||
|
||||
---
|
||||
|
||||
#### Data Bindings
|
||||
|
||||
Let's implement a **data binding** on the text input. Data bindings connect a DOM element to a Javascript variable so that updating one updates the other.
|
||||
|
||||
```javascript
|
||||
//binding a model value to an input in a template
|
||||
m("input", {value: todo.vm.description()})
|
||||
```
|
||||
|
||||
This binds the `description` getter-setter to the text input. Updating the value of the description in the model updates the DOM input when Mithril redraws.
|
||||
|
||||
```javascript
|
||||
todo.vm.init();
|
||||
|
||||
todo.vm.description(); // empty string
|
||||
m.render(document, todo.view()); // input is blank
|
||||
|
||||
todo.vm.description("Write code"); //set the description in the controller
|
||||
m.render(document, todo.view()); // input now says "Write code"
|
||||
```
|
||||
|
||||
At a glance it may seem like we're doing something very expensive by redrawing, but as it turns out, calling the `todo.view` method multiple times does not actually re-render the entire template. Internally, Mithril keeps a virtual representation of the DOM in cache, scans for changes, and then only modifies the absolute minimum required to apply the change to the DOM. In practice, this results in surprisingly fast re-rendering.
|
||||
|
||||
In the case above, Mithril only touches the `value` attribute of the input.
|
||||
|
||||
Note that the example above only *sets* the value of the input element in the DOM, but it never *reads* it. This means that typing something on the input and then re-rendering will clobber the text on screen.
|
||||
|
||||
---
|
||||
|
||||
Fortunately, bindings can also be **bi-directional**: that is, they can be coded in such a way that, in addition to setting the DOM value, it's also possible to read it as a user types, and then update the `description` getter-setter in the view-model.
|
||||
|
||||
Here's the most basic way of implementing the view-to-model part of the binding:
|
||||
|
||||
```javascript
|
||||
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()})
|
||||
```
|
||||
|
||||
The code bound to the `onchange` can be read like this: "with the attribute value, set todo.vm.description".
|
||||
|
||||
Note that Mithril does not prescribe how the binding updates: you can bind it to `onchange`, `onkeypress`, `oninput`, `onblur` or any other event that you prefer.
|
||||
|
||||
You can also specify what attribute to bind. This means that just as you are able to bind the `value` attribute in an `<select>`, you are also able to bind the `selectedIndex` property, if needed for whatever reason.
|
||||
|
||||
The `m.withAttr` utility is a functional programming tool provided by Mithril to minimize the need for anonymous functions in the view.
|
||||
|
||||
The `m.withAttr("value", todo.vm.description)` call above returns a function that is the rough equivalent of this code:
|
||||
|
||||
```javascript
|
||||
onchange: function(e) {
|
||||
todo.vm.description(e.target["value"]);
|
||||
}
|
||||
```
|
||||
|
||||
The difference, aside from avoiding an anonymous function, is that the `m.withAttr` idiom also takes care of catching the correct event target and selecting the appropriate source of the data - i.e. whether it should come from a Javascript property or from `DOMElement::getAttribute()`
|
||||
|
||||
---
|
||||
|
||||
In addition to bi-directional data binding, we can also bind parameterized functions to events:
|
||||
|
||||
```javascript
|
||||
var vm = todo.vm
|
||||
|
||||
m("button", {onclick: vm.add.bind(vm, vm.description)}, "Add")
|
||||
```
|
||||
|
||||
In the code above, we are simply using the native Javascript `Function::bind` method. This creates a new function with the parameter already set. In functional programming, this is called [*partial application*](http://en.wikipedia.org/wiki/Partial_application).
|
||||
|
||||
The `vm.add.bind(vm, vm.description)` expression above returns a function that is equivalent to this code:
|
||||
|
||||
```javascript
|
||||
onclick: function(e) {
|
||||
todo.vm.add(todo.vm.description)
|
||||
}
|
||||
```
|
||||
|
||||
Note that when we construct the parameterized binding, we are passing the `description` getter-setter *by reference*, and not its value. We only evaluate the getter-setter to get its value in the controller method. This is a form of *lazy evaluation*: it allows us to say "use this value later, when the event handler gets called".
|
||||
|
||||
Hopefully by now, you're starting to see why Mithril encourages the usage of `m.prop`: Because Mithril getter-setters are functions, they naturally compose well with functional programming tools, and allow for some very powerful idioms. In this case, we're using them in a way that resembles C pointers.
|
||||
|
||||
Mithril uses them in other interesting ways elsewhere.
|
||||
|
||||
Clever readers will probably notice that we can refactor the `add` method to make it much simpler:
|
||||
|
||||
```javascript
|
||||
vm.add = function() {
|
||||
if (vm.description()) {
|
||||
vm.list.push(new todo.Todo({description: vm.description()}));
|
||||
vm.description("");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The difference with the modified version is that `add` no longer takes an argument.
|
||||
|
||||
With this, we can make the `onclick` binding on the template *much* simpler:
|
||||
|
||||
```
|
||||
m("button", {onclick: todo.vm.add}, "Add")
|
||||
```
|
||||
|
||||
The only reason I talked about partial application here was to make you aware of that technique, since it becomes useful when dealing with parameterized event handlers. In real life, given a choice, you should always pick the simplest idiom for your use case.
|
||||
|
||||
---
|
||||
|
||||
To implement flow control in Mithril views, we simply use Javascript Array methods:
|
||||
|
||||
```javascript
|
||||
//here's the view
|
||||
m("table", [
|
||||
todo.vm.list.map(function(task, index) {
|
||||
return m("tr", [
|
||||
m("td", [
|
||||
m("input[type=checkbox]")
|
||||
]),
|
||||
m("td", task.description()),
|
||||
])
|
||||
})
|
||||
])
|
||||
```
|
||||
|
||||
In the code above, `todo.vm.list` is an Array, and `map` is one of its native functional methods. It allows us to iterate over the list and merge transformed versions of the list items into an output array.
|
||||
|
||||
As you can see, we return a partial template with two `<td>`'s. The second one has a data binding to the `description` getter-setter of the Todo class instance.
|
||||
|
||||
You're probably starting to notice that Javascript has strong support for functional programming and that it allows us to naturally do things that can be clunky in other frameworks (e.g. looping inside a `<dl>/<dt>/<dd>` construct).
|
||||
|
||||
---
|
||||
|
||||
The rest of the code can be implemented using idioms we already covered. The complete view looks like this:
|
||||
|
||||
```javascript
|
||||
todo.view = function() {
|
||||
return [
|
||||
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
|
||||
m("button", {onclick: todo.vm.add}, "Add"),
|
||||
m("table", [
|
||||
todo.vm.list.map(function(task, index) {
|
||||
return m("tr", [
|
||||
m("td", [
|
||||
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
|
||||
]),
|
||||
m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description()),
|
||||
])
|
||||
})
|
||||
])
|
||||
];
|
||||
};
|
||||
```
|
||||
|
||||
Here are the highlights of the template above:
|
||||
|
||||
- The template is rendered as a child of the document's `<body>`.
|
||||
- The text input saves its value to the `todo.vm.description` getter-setter we defined earlier.
|
||||
- The button calls the `todo.vm.add` method when clicked.
|
||||
- The table lists all the existing to-dos, if any.
|
||||
- The checkboxes save their value to the `task.done` getter setter.
|
||||
- The description gets crossed out via CSS if the task is marked as done.
|
||||
- When updates happen, the template is not wholly re-rendered - only the changes are applied.
|
||||
|
||||
---
|
||||
|
||||
So far, we've been using `m.render` to manually redraw after we made a change to the data. However, as I mentioned before, you can enable an [auto-redrawing system](auto-redrawing.md), by initializing the `todo` component via `m.mount`.
|
||||
|
||||
```javascript
|
||||
//render the todo component inside the body DOM node
|
||||
m.mount(document.body, {controller: todo.controller, view: todo.view});
|
||||
```
|
||||
|
||||
Mithril's auto-redrawing system keeps track of controller stability, and only redraws the view once it detects that the controller has finished running all of its code, including asynchronous AJAX payloads. Likewise, it intelligently waits for asynchronous services inside event handlers to complete before redrawing.
|
||||
|
||||
You can learn more about how redrawing heuristics work [here](auto-redrawing.md).
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
Here's the application code in its entirety:
|
||||
|
||||
```markup
|
||||
<!doctype html>
|
||||
<script src="mithril.min.js"></script>
|
||||
<body>
|
||||
<script>
|
||||
//this application only has one component: todo
|
||||
var todo = {};
|
||||
|
||||
//for simplicity, we use this component to namespace the model classes
|
||||
|
||||
//the Todo class has two properties
|
||||
todo.Todo = function(data) {
|
||||
this.description = m.prop(data.description);
|
||||
this.done = m.prop(false);
|
||||
};
|
||||
|
||||
//the TodoList class is a list of Todo's
|
||||
todo.TodoList = Array;
|
||||
|
||||
//the view-model tracks a running list of todos,
|
||||
//stores a description for new todos before they are created
|
||||
//and takes care of the logic surrounding when adding is permitted
|
||||
//and clearing the input after adding a todo to the list
|
||||
todo.vm = (function() {
|
||||
var vm = {}
|
||||
vm.init = function() {
|
||||
//a running list of todos
|
||||
vm.list = new todo.TodoList();
|
||||
|
||||
//a slot to store the name of a new todo before it is created
|
||||
vm.description = m.prop("");
|
||||
|
||||
//adds a todo to the list, and clears the description field for user convenience
|
||||
vm.add = function() {
|
||||
if (vm.description()) {
|
||||
vm.list.push(new todo.Todo({description: vm.description()}));
|
||||
vm.description("");
|
||||
}
|
||||
};
|
||||
}
|
||||
return vm
|
||||
}())
|
||||
|
||||
//the controller defines what part of the model is relevant for the current page
|
||||
//in our case, there's only one view-model that handles everything
|
||||
todo.controller = function() {
|
||||
todo.vm.init()
|
||||
}
|
||||
|
||||
//here's the view
|
||||
todo.view = function() {
|
||||
return [
|
||||
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
|
||||
m("button", {onclick: todo.vm.add}, "Add"),
|
||||
m("table", [
|
||||
todo.vm.list.map(function(task, index) {
|
||||
return m("tr", [
|
||||
m("td", [
|
||||
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
|
||||
]),
|
||||
m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description()),
|
||||
])
|
||||
})
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
//initialize the application
|
||||
m.mount(document.body, {controller: todo.controller, view: todo.view});
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
This example is also available as a [jsFiddle](http://jsfiddle.net/fbgypzbr/16/).
|
||||
There is also [Extended example](http://jsfiddle.net/glebcha/q7tvLxsa/) available on jsfiddle.
|
||||
|
||||
---
|
||||
|
||||
## Notes on Architecture
|
||||
|
||||
Idiomatic Mithril code is meant to apply good programming conventions and be easy to refactor.
|
||||
|
||||
In the application above, notice how the Todo class can easily be moved to a different component if code re-organization is required.
|
||||
|
||||
Todos are self-contained and their data aren't tied to the DOM like in typical jQuery based code. The Todo class API is reusable and unit-test friendly, and in addition, it's a plain-vanilla Javascript class, and so has almost no framework-specific learning curve.
|
||||
|
||||
[`m.prop`](mithril.prop.md) is a simple but surprisingly versatile tool: it's functionally composable, it enables [uniform data access](http://en.wikipedia.org/wiki/Uniform_data_access) and allows a higher degree of decoupling when major refactoring is required.
|
||||
|
||||
When refactoring is unavoidable, the developer can simply replace the `m.prop` call with an appropriate getter-setter implementation, instead of having to grep for API usage across the entire application.
|
||||
|
||||
For example, if todo descriptions needed to always be uppercased, one could simply change the `description` getter-setter:
|
||||
|
||||
```javascript
|
||||
this.description = m.prop(data.description)
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```javascript
|
||||
//private store
|
||||
var description;
|
||||
|
||||
//public getter-setter
|
||||
this.description = function(value) {
|
||||
if (arguments.length > 0) description = value.toUpperCase();
|
||||
return description;
|
||||
}
|
||||
|
||||
//make it serializable
|
||||
this.description.toJSON = function() {return description}
|
||||
|
||||
//set the value
|
||||
this.description(data.description)
|
||||
```
|
||||
|
||||
In the view-model, we aliased the native Array class for `TodoList`. Be aware that by using the native Array class, we're making an implicit statement that we are going to support all of the standard Array methods as part of our API.
|
||||
|
||||
While this decision allows better API discoverability, the trade-off is that we're largely giving up on custom constraints and behavior. For example, if we wanted to change the application to make the list be persisted, a native Array would most certainly not be a suitable class to use.
|
||||
|
||||
In order to deal with that type of refactoring, one can explicitly decide to support only a subset of the Array API, and implement another class with the same interface as this subset API.
|
||||
|
||||
Given the code above, the replacement class would only need to implement the `.push()` and `.map()` methods. By freezing APIs and swapping implementations, the developer can completely avoid touching other layers in the application while refactoring.
|
||||
|
||||
```javascript
|
||||
todo.TodoList = Array;
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```javascript
|
||||
todo.TodoList = function () {
|
||||
this.push = function() { /*...*/ },
|
||||
this.map = function() { /*...*/ }
|
||||
};
|
||||
```
|
||||
|
||||
Hopefully these examples give you an idea of ways requirements can change over time and how Mithril's philosophy allows developers to use standard OOP techniques to refactor their codebases, rather than needing to modify large portions of the application.
|
||||
|
||||
---
|
||||
|
||||
The first and most obvious thing you may have noticed in the view layer is that the view is not written in HTML.
|
||||
|
||||
While superficially this may seem like an odd design, this actually has a lot of benefits:
|
||||
|
||||
- No flash-of-unbehaviored-content (FOUC). In fact, Mithril is able to render a fully functional application - with working event handlers - before the "DOM ready" event fires!
|
||||
|
||||
- There's no need for a parse-and-compile pre-processing step to turn strings containing HTML + templating syntax into working DOM elements.
|
||||
|
||||
- Mithril views can provide accurate and informative error reporting, with line numbers and meaningful stack traces.
|
||||
|
||||
- You get the ability to automate linting, unit testing and minifying of the entire view layer.
|
||||
|
||||
- It provides full Turing completeness: full control over evaluation eagerness/laziness and caching in templates. You can even build components that take other components as first-class-citizen parameters!
|
||||
|
||||
And if you really do want to use HTML syntax after all, [you can use a package called MSX](https://github.com/insin/msx).
|
||||
|
||||
Views in Mithril use a virtual DOM diff implementation, which sidesteps performance problems related to opaque dirty-checking and excessive browser repaint that are present in some frameworks.
|
||||
|
||||
Another feature - the optional `m()` utility - allows writing terse templates in a declarative style using CSS shorthands, similar to popular HTML preprocessors from server-side MVC frameworks.
|
||||
|
||||
And because Mithril views are Javascript, the developer has full freedom to abstract common patterns - from bidirectional binding helpers to full blown components - using standard Javascript refactoring techniques.
|
||||
|
||||
Mithril templates are also more collision-proof than other component systems since there's no way to pollute the HTML tag namespace by defining ad-hoc tag names.
|
||||
|
||||
A more intellectually interesting aspect of the framework is that event handling is encouraged to be done via functional composition (i.e. by using tools like [`m.withAttr`](mithril.withAttr.md), [`m.prop`](mithril.prop.md) and the native `.bind()` method for partial application).
|
||||
|
||||
If you've been interested in learning or using Functional Programming in the real world, Mithril provides very pragmatic opportunities to get into it.
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
Mithril provides a few more facilities that are not demonstrated in this page. The following topics are good places to start a deeper dive.
|
||||
|
||||
- [Routing](routing.md)
|
||||
- [Web Services](web-services.md)
|
||||
- [Components](components.md)
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
- [Optimizing performance](optimizing-performance.md)
|
||||
- [Integrating with the Auto-Redrawing System](auto-redrawing.md)
|
||||
- [Integrating with Other Libraries](integration.md)
|
||||
|
||||
## Misc
|
||||
|
||||
- [Differences from Other MVC Frameworks](comparison.md)
|
||||
- [Benchmarks](benchmarks.md)
|
||||
- [Good Practices](practices.md)
|
||||
- [Useful Tools](tools.md)
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
## Integrating with Other Libraries
|
||||
|
||||
Integration with third party libraries or vanilla javascript code can be achieved via the [`config` attribute of virtual elements](mithril.md#accessing-the-real-dom).
|
||||
|
||||
It's recommended that you encapsulate integration code in a component or a helper function.
|
||||
|
||||
The example below shows a simple component that integrates with the [select2 library](http://ivaynberg.github.io/select2/).
|
||||
|
||||
```javascript
|
||||
// Component containing a Select that uses Select2 for functionality.
|
||||
var S2Component = {
|
||||
|
||||
// Rendered view for S2
|
||||
view: function(ctrl, attrs) {
|
||||
var current = attrs.selectedUser
|
||||
|
||||
return m('select', {
|
||||
class: 'select-field',
|
||||
config: S2Component._configure
|
||||
},
|
||||
attrs.data.map(function(item){
|
||||
return m('option', {
|
||||
id: item.id,
|
||||
value: item.value,
|
||||
selected: (item.id === current) ? true : false
|
||||
}, item.name);
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
// Configure function -- called from m('select') in the view
|
||||
_configure: function(element, initialized) {
|
||||
|
||||
/*
|
||||
Note: This function is being called from the 'config' attribute
|
||||
in our mithril view.
|
||||
|
||||
Integration with third-party party DOM manipulation (jQuery) needs
|
||||
{config: function(){}} because that's the attribute that exposes the real
|
||||
DOM element (as opposed to the virtual DOM element) in the corresponding
|
||||
function so you can access and manipulate it.
|
||||
*/
|
||||
|
||||
// If this hasn't been initialized, we can do our setup
|
||||
if(!initialized) {
|
||||
|
||||
$(element).select2({
|
||||
// options
|
||||
});
|
||||
|
||||
// Other logic pertaining to this select also goes here.
|
||||
// e.g. Event handlers, etc.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary component.
|
||||
var MainComponent = {
|
||||
controller: function() {
|
||||
var ctrl = this;
|
||||
|
||||
// Some arbitrary data
|
||||
// Aaron Burr is the initially selected user.
|
||||
ctrl.selectedUser = 2;
|
||||
ctrl.data = [
|
||||
{id: 1, name: 'Alexander Hamilton'},
|
||||
{id: 2, name: 'Aaron Burr'},
|
||||
{id: 3, name: 'Thomas Jefferson'},
|
||||
{id: 4, name: 'John Adams'},
|
||||
{id: 5, name: 'James Madison'},
|
||||
{id: 6, name: 'Elizabeth Schuyler'},
|
||||
{id: 7, name: 'King George'},
|
||||
{id: 8, name: 'Marquis de Lafayette'}
|
||||
]
|
||||
},
|
||||
|
||||
view: function(ctrl) {
|
||||
return m('div', {class: 'select-container'}, [
|
||||
m('label', 'Historical Figure: '),
|
||||
m(S2Component, {
|
||||
selectedUser: ctrl.selectedUser,
|
||||
data: ctrl.data
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MainComponent)
|
||||
|
||||
```
|
||||
|
||||
[Source code in JSFiddle](https://jsfiddle.net/11pz8afy/9/)
|
||||
|
||||
|
||||
`_configure` is a helper function that is called via the `config` attribute in the `select` we render in our `SC2Component.view`
|
||||
|
||||
This `_configure` function has a guarded `if` statement: `if(!initialized)`. In the event this component is being instantiated for the first time, we're going to do all of the initial setup on the first render. Subsequent redraws will __not__ run the initialization code again, making sure everything in the `_configure` function is only initialized once.
|
||||
|
||||
The initialization code is simply calling `$(element).select2()` on the exposed DOM element in order to initialize it. You can also addevent handlers to these elements. If you modify the DOM or any data that your component relies on inside of this function, you'll need to make sure the component knows to redraw itself by adding `m.redraw` as needed.
|
||||
|
||||
`m.startComputation` and `m.endComputation` are used for asynchronous operations. If you were to call a web service using jQuery, then you would be responsible for adding a `m.startComputation` call before the jQuery ajax call, and for adding a `m.endComputation` call at the end of the completion callback, in addition to the calls within any event handlers. Refer to the [`auto-redrawing`](auto-redrawing.md) guide for an example.
|
||||
|
||||
Though possible, you should avoid calling `m.redraw`, `m.startComputation` and `m.endComputation` in the `_configure` function's execution thread. (An execution thread is basically any amount of code that runs before other asynchronous threads start to run.) Relying on multiple redraw passes degrades performance and makes it possible to code yourself into an infinite execution loop situation, which is extremely difficult to debug.
|
||||
|
||||
The component in the example shows how a developer would consume the `SC2Component`.
|
||||
|
||||
You should always document integration components so that others can find out what attribute parameters can be used to initialize the component.
|
||||
|
||||
---
|
||||
|
||||
## Integrating to legacy code
|
||||
|
||||
If you need to add separate widgets to different places on a same page, you can simply initialize each widget as you would a regular Mithril application (i.e. use `m.render`, `m.mount` or `m.route`).
|
||||
|
||||
There's just one caveat: while simply initializing multiple "islands" in this fashion works, their initialization calls are not aware of each other and can cause redraws too frequently. To optimize rendering, you should add a `m.startComputation` call before the first widget initialization call, and a `m.endComputation` after the last widget initialization call in each execution thread.
|
||||
|
||||
```javascript
|
||||
m.startComputation()
|
||||
|
||||
m.mount(document.getElementById("widget1-container"), Widget1)
|
||||
|
||||
m.mount(document.getElementById("widget2-container"), Widget2)
|
||||
|
||||
m.endComputation()
|
||||
```
|
||||
|
|
@ -1,605 +0,0 @@
|
|||
## m.component
|
||||
|
||||
---
|
||||
|
||||
- [Rendering components](#rendering-components)
|
||||
- [Optional controller](#optional-controller)
|
||||
- [Controller as a class constructor](#controller-as-a-class-constructor)
|
||||
- [Notes on the view function](#notes-on-the-view-function)
|
||||
- [Shorthand syntax](#shorthand-syntax)
|
||||
- [Parameterized components](#parameterized-components)
|
||||
- [Nesting components](#nesting-components)
|
||||
- [Dealing with state](#dealing-with-state)
|
||||
- [Stateless components](#stateless-components)
|
||||
- [Stateful components](#stateful-components)
|
||||
- [Parameterized initial state](#parameterized-initial-state)
|
||||
- [Data-driven component identity](#data-driven-component-identity)
|
||||
- [Unloading/Unmounting components](#unloading-components)
|
||||
- [Nested asynchronous components](#nested-asynchronous-components)
|
||||
- [Limitations and caveats](#limitations-and-caveats)
|
||||
- [Opting out of the auto redrawing system](#opting-out-of-the-auto-redrawing-system)
|
||||
- [Signature](#signature)
|
||||
|
||||
---
|
||||
|
||||
Components are building blocks for Mithril applications. They allow developers to encapsulate functionality into reusable units.
|
||||
|
||||
---
|
||||
|
||||
### Rendering components
|
||||
|
||||
In Mithril, a component is nothing more than an object that has a `view` function and, optionally, a `controller` function.
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function(data) {
|
||||
return {greeting: "Hello"}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("h1", ctrl.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent) // renders <h1>Hello</h1> into <body>
|
||||
```
|
||||
|
||||
The optional `controller` function creates an object that may be used in the following recommended ways:
|
||||
|
||||
- It can contain methods meant to be called by a `view`.
|
||||
- It can call model methods directly or from methods inside the resulting object.
|
||||
- It can store contextual data returned from model methods (i.e. a [promise](mithril.deferred.md) from a [request](mithril.request.md)).
|
||||
- It can hold a reference to a view model.
|
||||
|
||||
The `view` has access to methods and properties that the controller chooses to expose in the returned object. With those methods and properties, it creates a template that can consume model data and call controller methods to affect the model. This is the recommended way for views and models to exchange data.
|
||||
|
||||
```javascript
|
||||
//a simple MVC example
|
||||
|
||||
//a sample model that exposes a value
|
||||
var model = {count: 0}
|
||||
|
||||
var MyComponent = {
|
||||
controller: function(data) {
|
||||
return {
|
||||
increment: function() {
|
||||
//This is a simplication for the sake of the example.
|
||||
//Typically, values are modified via model methods,
|
||||
//rather than modified directly
|
||||
model.count++
|
||||
}
|
||||
}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("a[href=javascript:;]", {
|
||||
onclick: ctrl.increment //view calls controller method on click
|
||||
}, "Count: " + model.count)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent)
|
||||
|
||||
//renders:
|
||||
//<a href="javascript:;">Count: 0</a>
|
||||
//
|
||||
//the number increments when the link is clicked
|
||||
```
|
||||
|
||||
Note that there is no requirement to tightly couple a controller and view while organizing code. It's perfectly valid to define controllers and views separately, and only bring them together when mounting them:
|
||||
|
||||
```javascript
|
||||
//controller.js
|
||||
var controller = function(data) {
|
||||
return {greeting: "Hello"}
|
||||
}
|
||||
|
||||
//view.js
|
||||
var view = function(ctrl) {
|
||||
return m("h1", ctrl.greeting)
|
||||
}
|
||||
|
||||
//render
|
||||
m.mount(document.body, {controller: controller, view: view}) // renders <h1>Hello</h1>
|
||||
```
|
||||
|
||||
There are three ways to render a component:
|
||||
|
||||
- [`m.route`](mithril.route.md) (if you are building a single-page application that has multiple pages)
|
||||
- [`m.mount`](mithril.mount.md) (if your app only has one page)
|
||||
- [`m.render`](mithril.render.md) (if you are integrating Mithril's rendering engine into a larger framework and wish to manage redrawing yourself).
|
||||
|
||||
The `controller` function is called *once* when the component is rendered. Subsequently, the `view` function is called and will be called again anytime a redraw is required. The return value of the `controller` function is passed to the `view` as its first argument.
|
||||
|
||||
#### Optional controller
|
||||
|
||||
The `controller` function is optional and defaults to an empty function `controller: function() {}`
|
||||
|
||||
```javascript
|
||||
//a component without a controller
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("h1", "Hello")
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent) // renders <h1>Hello</h1>
|
||||
```
|
||||
|
||||
#### Controller as a class constructor
|
||||
|
||||
A controller can also be used as a class constructor (i.e. it's possible to attach properties to the `this` object within the constructor, instead of returning a value).
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function(data) {
|
||||
this.greeting = "Hello"
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("h1", ctrl.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyComponent) // renders <h1>Hello</h1>
|
||||
```
|
||||
|
||||
#### Notes on the view function
|
||||
|
||||
The `view` function does not create a DOM tree when called. The return value of the view function is merely a plain Javascript data structure that represents a DOM tree. Internally, Mithril uses this data representation of the DOM to probe for data changes and update the DOM only where necessary. This rendering technique is known as *virtual DOM diffing*.
|
||||
|
||||
The view function is run again whenever a redraw is required (i.e. whenever event handlers are triggered by user input). Its return value is used to diff against the previous virtual DOM tree.
|
||||
|
||||
It may sound expensive to recompute an entire view any time there's a change to be displayed, but this operation actually turns out to be quite fast, compared to rendering strategies used by older frameworks. Mithril's diffing algorithm makes sure expensive DOM operations are performed only if absolutely necessary, and as an extra benefit, the global nature of the redraw makes it easy to reason about and troubleshoot the state of the application.
|
||||
|
||||
### Shorthand syntax
|
||||
|
||||
If the first argument to `m()` is a component, it acts as an alias of `m.component()`
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function() {
|
||||
return {greeting: "hello"}
|
||||
},
|
||||
view: function(ctrl, args) {
|
||||
return m("h1", ctrl.greeting + " " + args.data)
|
||||
}
|
||||
}
|
||||
|
||||
m.render(document.body, [
|
||||
//the two lines below are equivalent
|
||||
m(MyComponent, {data: "world"}),
|
||||
m.component(MyComponent, {data: "world"})
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Parameterized components
|
||||
|
||||
Components can have arguments "preloaded". In practice, this means that calling `m.component(MyComponent, {foo: "bar"})` will return a component that behaves exactly the same as `MyComponent`, but `{foo: "bar"}` will be bound as an argument to both the `controller` and `view` functions.
|
||||
|
||||
```javascript
|
||||
//declare a component
|
||||
var MyComponent = {
|
||||
controller: function(args, extras) {
|
||||
console.log(args.name, extras)
|
||||
return {greeting: "Hello"}
|
||||
},
|
||||
view: function(ctrl, args, extras) {
|
||||
return m("h1", ctrl.greeting + " " + args.name + " " + extras)
|
||||
}
|
||||
}
|
||||
|
||||
//create a component whose controller and view functions receive some arguments
|
||||
var component = m.component(MyComponent, {name: "world"}, "this is a test")
|
||||
|
||||
var ctrl = new component.controller() // logs "world", "this is a test"
|
||||
|
||||
m.render(document.body, component.view(ctrl)) // render the virtual DOM tree manually
|
||||
|
||||
//<body><h1>Hello world this is a test</h1></body>
|
||||
```
|
||||
|
||||
The first parameter after the component object is meant to be used as an attribute map and should be an object (e.g. `{name: "world"}`). Subsequent parameters have no restrictions (e.g. `"this is a test"`)
|
||||
|
||||
---
|
||||
|
||||
### Nesting components
|
||||
|
||||
Component views can include other components:
|
||||
|
||||
```javascript
|
||||
var App = {
|
||||
view: function() {
|
||||
return m(".app", [
|
||||
m("h1", "My App"),
|
||||
|
||||
//nested component
|
||||
m.component(MyComponent, {message: "Hello"})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
controller: function(args) {
|
||||
return {greeting: args.message}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("h2", ctrl.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, App)
|
||||
|
||||
// <div class="app">
|
||||
// <h1>My App</h1>
|
||||
// <h2>Hello</h2>
|
||||
// </div>
|
||||
```
|
||||
|
||||
Components can be placed anywhere a regular element can. If you have components inside a sortable list, you should add `key` attributes to your components to ensure that DOM elements are not recreated from scratch, but merely moved when possible. Keys must be unique within a list of sibling DOM elements, and they must be either a string or a number:
|
||||
|
||||
```javascript
|
||||
var App = {
|
||||
controller: function() {
|
||||
return {data: [1, 2, 3]}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m(".app", [
|
||||
//pressing the button reverses the list
|
||||
m("button[type=button]", {onclick: function() {ctrl.data.reverse()}}, "My App"),
|
||||
|
||||
ctrl.data.map(function(item) {
|
||||
//the key ensures the components aren't recreated from scratch, if they merely exchanged places
|
||||
return m.component(MyComponent, {message: "Hello " + item, key: item})
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
controller: function(args) {
|
||||
return {greeting: args.message}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("h2", ctrl.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, App)
|
||||
```
|
||||
|
||||
### Dealing with state
|
||||
|
||||
#### Stateless components
|
||||
|
||||
A component is said to be stateless when it does not store data internally. Instead, it's composed of [pure functions](http://en.wikipedia.org/wiki/Pure_function). It's a good practice to make components stateless because they are more predictable, and easier to reason about, test and troubleshoot.
|
||||
|
||||
Instead of copying arguments to the controller object and then passing the controller object to the view (thereby creating internal state in the component), it is often desirable that views update based on the current value of arguments initially passed to a component.
|
||||
|
||||
The following example illustrates this pattern:
|
||||
|
||||
```javascript
|
||||
var MyApp = {
|
||||
controller: function() {
|
||||
return {
|
||||
temp: m.prop(10) // kelvin
|
||||
}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("div", [
|
||||
m("input", {oninput: m.withAttr("value", ctrl.temp), value: ctrl.temp()}), "K",
|
||||
m("br"),
|
||||
m.component(TemperatureConverter, {value: ctrl.temp()})
|
||||
]);
|
||||
}
|
||||
};
|
||||
var TemperatureConverter = {
|
||||
controller: function() {
|
||||
//note how the controller does not handle the input arguments
|
||||
|
||||
//define some helper functions to be called from the view
|
||||
return {
|
||||
kelvinToCelsius: function(value) {
|
||||
return value - 273.15
|
||||
},
|
||||
kelvinToFahrenheit: function(value) {
|
||||
return (9 / 5 * (value - 273.15)) + 32
|
||||
}
|
||||
}
|
||||
},
|
||||
view: function(ctrl, args) {
|
||||
return m('div', [
|
||||
"celsius:", ctrl.kelvinToCelsius(args.value),
|
||||
m("br"),
|
||||
"fahrenheit:", ctrl.kelvinToFahrenheit(args.value),
|
||||
]);
|
||||
}
|
||||
};
|
||||
m.mount(document.body, MyApp);
|
||||
```
|
||||
|
||||
In the example above, the text input is bi-directionally bound to a `temp` getter-setter. Changing the temperature value from the input updates the temperature value, which is passed to the TemperatureConverter view directly, and transformation functions are called from there. The TemperatureConverter controller never stores the value.
|
||||
|
||||
Testing the various parts of the component is trivial:
|
||||
|
||||
```javascript
|
||||
//test a transformation function in the controller
|
||||
var ctrl = new TemperatureConverter.controller();
|
||||
assert(ctrl.kelvinToCelsius(273.15) == 0)
|
||||
|
||||
//test the template
|
||||
var tpl = TemperatureConverter.view(ctrl, {value: 273.15})
|
||||
assert(tpl.children[1] == 0)
|
||||
|
||||
//test with real DOM
|
||||
var testRoot = document.createElement("div")
|
||||
m.render(testRoot, TemperatureConverter.view(ctrl, {value: 273.15}))
|
||||
assert(testRoot.innerHTML.indexOf("celsius:0") > -1)
|
||||
```
|
||||
|
||||
Note that the sample component above is illustrative. Ideally, temperature conversion functions (and any functions that deal strictly within the domain of the data) should go in the model layer, not in a component's controller.
|
||||
|
||||
---
|
||||
|
||||
### Stateful components
|
||||
|
||||
Usually it's recommended that you store application state outside of components (either in a [view-model](http://lhorie.github.io/mithril-blog/what-is-a-view-model.html) or in the top-level component in the case of nested components). Components *can* be stateful, but the purpose of component state is to prevent the pollution of the model layer with aspects that are inherently related to the component. For example, an autocompleter component may need to internally store a flag to indicate whether the dropdown is visible, but this kind of state is not relevant to an application's business logic.
|
||||
|
||||
You might also elect to maintain component state when it's not meaningful outside the scope of a single component. For example, you might have a `UserForm` component that lives alongside other unrelated components on a bigger page, but it probably doesn't make sense for the parent page to be aware of the unsaved user data stored within the `UserForm` component.
|
||||
|
||||
---
|
||||
|
||||
#### Parameterized initial state
|
||||
|
||||
The ability to handle arguments in the controller is useful for setting up the initial state for a component whose state depends on input data:
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function(args) {
|
||||
//we only want to make this call once
|
||||
return {
|
||||
things: m.request({method: "GET", url: "/api/things/", data: args}) //slice the data in some way
|
||||
}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("ul", [
|
||||
ctrl.things().map(function(thing) {
|
||||
return m("li", thing.name)
|
||||
})
|
||||
]);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
However, it's recommended that you aggregate all of your requests in a single place instead of scattering them across multiple components. Aggregating requests in a top-level component makes it easier to replay the request chain (i.e. fetching an updated list of items after you've saved something that changes that list), and it ensures the entire data set is loaded in memory before drilling down into nested components, avoiding redundant AJAX calls for sibling components that need the same data. Be sure to read the [Application Architecture section](components.md#application-architecture-with-components) to learn more about organizing componentized code.
|
||||
|
||||
---
|
||||
|
||||
#### Data-driven component identity
|
||||
|
||||
A component can be re-initialized from scratch by changing the `key` associated with it. This is useful for re-running ajax calls for different model entities.
|
||||
|
||||
Suppose we have a component called `ProjectList` and the following data:
|
||||
|
||||
```javascript
|
||||
|
||||
var people = [
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Mary"}
|
||||
]
|
||||
|
||||
//ajax and display a list of projects for John
|
||||
m.render(document.body, m(ProjectList, {key: people[0].id, value: people[0]}))
|
||||
|
||||
//ajax and display a list of projects for Mary
|
||||
m.render(document.body, m(ProjectList, {key: people[1].id, value: people[1]}))
|
||||
```
|
||||
|
||||
In the example above, since the key is different, the ProjectList component is recreated from scratch. As a result, the controller runs again, the DOM is re-generated, and any applicable 3rd party plugins in configs are re-initialized.
|
||||
|
||||
Remember that the rules for keys apply to components the same way they do to regular elements: it is not allowed to have duplicate keys on children of the same parent, and they must be either strings or numbers (or something with a `.toString()` implementation that makes the entity uniquely identifiable in the local scope when serialized). You can learn more about keys [here](mithril.md#dealing-with-focus).
|
||||
|
||||
---
|
||||
|
||||
### Unloading components
|
||||
|
||||
If a component's controller contains the function `onunload`, it will be called under one of these circumstances:
|
||||
|
||||
- when a new call to `m.mount` updates the root DOM element of the component in question
|
||||
- when a route changes (if you are using [`m.route`](mithril.route.md))
|
||||
|
||||
To unload/unmount a component without loading another component, you can simply call `m.mount` with a `null` as the component parameter:
|
||||
|
||||
```javascript
|
||||
m.mount(rootElement, null);
|
||||
```
|
||||
|
||||
Often, you will want to do some work before the component is unloaded (i.e. clear timers or unsubscribe event handlers):
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function() {
|
||||
return {
|
||||
onunload: function() {
|
||||
console.log("unloading my component");
|
||||
}
|
||||
}
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "test")
|
||||
}
|
||||
};
|
||||
|
||||
m.mount(document.body, MyComponent);
|
||||
|
||||
//...
|
||||
|
||||
var AnotherComponent = {
|
||||
view: function() {
|
||||
return m("div", "another")
|
||||
}
|
||||
};
|
||||
|
||||
// mount on the same DOM element, replacing MyComponent
|
||||
m.mount(document.body, AnotherComponent); // logs "unloading my component"
|
||||
```
|
||||
|
||||
You can also use the `onunload` function to PREVENT a component from being unloaded in the context of a route change (i.e. to alert a user to save their changes before navigating away from a page)
|
||||
|
||||
```javascript
|
||||
var component = {
|
||||
controller: function() {
|
||||
var unsaved = m.prop(false)
|
||||
return {
|
||||
unsaved: unsaved,
|
||||
|
||||
onunload: function(e) {
|
||||
if (unsaved()) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Normally, calling `m.mount` will return the controller instance for that component, but there's one corner case: if `e.preventDefault()` is called from a controller's `onunload` method, then the `m.mount` call will not instantiate the new controller, and will return `undefined`.
|
||||
|
||||
Mithril does not hook into the browser's `onbeforeunload` event. To prevent unloading when attempting to navigate away from a page, you can check the return value of `m.mount`
|
||||
|
||||
```javascript
|
||||
window.onbeforeunload = function() {
|
||||
if (!m.mount(rootElement, null)) {
|
||||
//onunload's preventDefault was called
|
||||
return "Are you sure you want to leave?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Components that are nested inside other components can also call `onunload` and its `e.preventDefault()` like top-level components. The `onunload` event is called if an instantiated component is removed from a virtual element tree via a redraw.
|
||||
|
||||
In the example below, clicking the button triggers the component's `onunload` event and logs "unloaded!".
|
||||
|
||||
```javascript
|
||||
var MyApp = {
|
||||
controller: function() {
|
||||
return {loaded: true}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return [
|
||||
m("button[type=button]", {onclick: function() {ctrl.loaded = false}}),
|
||||
ctrl.loaded ? MyComponent : ""
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
controller: function() {
|
||||
return {
|
||||
onunload: function() {
|
||||
console.log("unloaded!")
|
||||
}
|
||||
}
|
||||
},
|
||||
view: function() {
|
||||
return m("h1", "My component")
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(document.body, MyApp)
|
||||
```
|
||||
|
||||
Calling `e.preventDefault()` from a component's `onunload` function aborts route changes, but it does not abort rollback or affect the current redraw in any way.
|
||||
|
||||
---
|
||||
|
||||
### Nested asynchronous components
|
||||
|
||||
Since controllers can call model methods, it's possible for nested components to encapsulate asynchronous behavior. When components aren't nested, Mithril waits for all asynchronous tasks to complete, but when components are nested, a component's parent view renders before the component completes its asynchronous tasks. The existence of the component only becomes known to the diff engine at the time when the template is rendered.
|
||||
|
||||
When a component has asynchronous payloads and they are queued by the [auto-redrawing system](auto-redrawing.md), its view is NOT rendered until all asynchronous operations complete. When the component's asynchronous operations complete, another redraw is triggered and the entire template tree is evaluated again. This means that the virtual dom tree may take two or more redraws (depending on how many nested asynchronous components there are) to be fully rendered.
|
||||
|
||||
There are [different ways to organize components](components.md#application-architecture-with-components) that can side-step the need for multiple redraws. Regardless, you could also force multiple redraws to happen by using the [`background`](mithril.request.md#rendering-before-web-service-requests-finish) and `initialValue` options in `m.request`, or by manually calling [`m.redraw()`](mithril.redraw.md).
|
||||
|
||||
If a component A contains another component B that calls asynchronous services, when component A is rendered, a `<placeholder>` tag is rendered in place of component B until B's asynchronous services resolve. Once resolved, the placeholder is replaced with component B's view.
|
||||
|
||||
---
|
||||
|
||||
### Limitations and caveats
|
||||
|
||||
One important limitation to be aware of when using components is that you cannot call Mithril's redrawing methods ([`m.startComputation` / `m.endComputation`](mithril.computation.md) and [`m.redraw`](mithril.redraw.md)) from templates.
|
||||
|
||||
In addition, you cannot call `m.request` from templates. Doing so will trigger another redraw, which will result in an infinite loop.
|
||||
|
||||
There are a few other technical caveats when nesting components:
|
||||
|
||||
1. Nested component views must return either a virtual element or another component. Returning an array, a string, a number, boolean, falsy value, etc will result in an error.
|
||||
|
||||
2. Nested components cannot change `m.redraw.strategy` from the controller constructor (but they can from event handlers). It's recommended that you use the [`ctx.retain`](mithril.md#persisting-dom-elements-across-route-changes) flag instead of changing the redraw strategy in controller constructors.
|
||||
|
||||
3. The root DOM element in a component's view must not be changed during the lifecycle of the component, otherwise undefined behavior will occur. In other words, don't do this:
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return someCondition ? m("a") : m("b")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. If a component's root element is a subtree directive on its first rendering pass, undefined behavior will occur.
|
||||
|
||||
---
|
||||
|
||||
### Opting out of the auto redrawing system
|
||||
|
||||
Components can be rendered without enabling the [auto-redrawing system](auto-redrawing.md), via [`m.render`](mithril.render.md):
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function() {
|
||||
return {greeting: "Hello"}
|
||||
},
|
||||
view: function(ctrl) {
|
||||
return m("h1", ctrl.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
m.render(document.body, MyComponent)
|
||||
```
|
||||
|
||||
However, using [`m.render`](mithril.render.md) is only recommended if you want to use Mithril as part of a larger framework that manages the rendering lifecycle on its own. The vast majority of times, it's advisable to use `m.mount` instead.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
[How to read signatures](how-to-read-signatures.md)
|
||||
|
||||
```clike
|
||||
Component component(Component component [, Object attributes [, any... args]])
|
||||
|
||||
where:
|
||||
Component :: Object { Controller, View }
|
||||
Controller :: SimpleController | UnloadableController
|
||||
SimpleController :: void controller([Object attributes [, any... args]])
|
||||
UnloadableController :: void controller([Object attributes [, any... args]]) { prototype: void unload(UnloadEvent e) }
|
||||
UnloadEvent :: Object {void preventDefault()}
|
||||
View :: void view(Object controllerInstance [, Object attributes [, any... args]])
|
||||
```
|
||||
|
||||
- **Component component**
|
||||
|
||||
A component is supposed to be an Object with two keys: `controller` and `view`. Each of these should point to a Javascript function. If a controller is not specified, Mithril will automatically create an empty controller function.
|
||||
|
||||
- **Object attributes**
|
||||
|
||||
A key/value map of attributes that gets bound as an argument to both the `controller` and `view` functions of the component.
|
||||
|
||||
- **any... args**
|
||||
|
||||
Other arguments to be bound as arguments to both the `controller` and `view` functions
|
||||
|
||||
- **returns Component parameterizedComponent**
|
||||
|
||||
A component with arguments bound
|
||||
671
docs/mithril.md
671
docs/mithril.md
|
|
@ -1,671 +0,0 @@
|
|||
## m
|
||||
|
||||
---
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Binding to data](#binding-to-data)
|
||||
- [Using HTML entities](#using-html-entities)
|
||||
- [Accessing the real DOM element](#accessing-the-real-dom-element)
|
||||
- [Persisting config data](#persisting-config-data)
|
||||
- [Destructors](#destructors)
|
||||
- [Persisting DOM elements across route changes](#persisting-dom-elements-across-route-changes)
|
||||
- [SVG](#svg)
|
||||
- [Dealing with focus](#dealing-with-focus)
|
||||
- [Dealing with sorting and deleting in lists](#dealing-with-sorting-and-deleting-in-lists)
|
||||
- [Component shorthand](#component-shorthand)
|
||||
- [Signature](#signature)
|
||||
- [The `config` attribute](#the-config-attribute)
|
||||
---
|
||||
|
||||
This is a convenience method to compose virtual elements that can be rendered via [`m.render()`](mithril.render.md).
|
||||
|
||||
You are encouraged to use CSS selectors to define virtual elements. See "Signature" section for details.
|
||||
|
||||
---
|
||||
|
||||
### Usage
|
||||
|
||||
You can use simple tag selectors to make templates resemble HTML:
|
||||
|
||||
```javascript
|
||||
m("br"); //yields a virtual element that represents <br>
|
||||
|
||||
m("div", "Hello"); //yields <div>Hello</div>
|
||||
|
||||
m("div", {class: "container"}, "Hello"); //yields <div class="container">Hello</div>
|
||||
```
|
||||
|
||||
Note that the output value from `m()` is not an actual DOM element. In order to turn the virtual element into a real DOM element, you must call [`m.render()`](mithril.render.md).
|
||||
|
||||
```javascript
|
||||
m.render(document.body, m("br")); //puts a <br> in <body>
|
||||
```
|
||||
|
||||
You can also use more complex CSS selectors:
|
||||
|
||||
```javascript
|
||||
m(".container"); //yields <div class="container"></div>
|
||||
|
||||
m("#layout"); //yields <div id="layout"></div>
|
||||
|
||||
m("a[name=top]"); //yields <a name="top"></a>
|
||||
|
||||
m("[contenteditable]"); //yields <div contenteditable></div>
|
||||
|
||||
m("a#google.external[href='http://google.com']", "Google"); //yields <a id="google" class="external" href="http://google.com">Google</a>
|
||||
```
|
||||
|
||||
Each `m()` call creates a virtual DOM element, that is, a Javascript object that represents a DOM element, and which is eventually converted into one.
|
||||
|
||||
You can, of course, nest virtual elements:
|
||||
|
||||
```javascript
|
||||
m("ul", [
|
||||
m("li", "item 1"),
|
||||
m("li", "item 2"),
|
||||
]);
|
||||
|
||||
/*
|
||||
yields
|
||||
<ul>
|
||||
<li>item 1</li>
|
||||
<li>item 2</li>
|
||||
</ul>
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The CSS selector syntax (e.g. `a#google.external[href='http://google.com']`) is meant to be used for declaring static attributes in the element, i.e. attribute values that don't change dynamically when the user interacts with the app.
|
||||
|
||||
The `attributes` argument (i.e. the second parameter in the `m("div", {class: "container"}, "Hello")` example) is meant to be used for attributes whose values we want to dynamically populate.
|
||||
|
||||
For example, let's say that you're generating a link from an entry that comes from a web service:
|
||||
|
||||
```javascript
|
||||
//assume the variable `link` came from a web service
|
||||
var link = {url: "http://google.com", title: "Google"}
|
||||
|
||||
m("a", {href: link.url}, link.title); //yields <a href="http://google.com">Google</a>
|
||||
```
|
||||
|
||||
Here's a less trivial example:
|
||||
|
||||
```javascript
|
||||
var links = [
|
||||
{title: "item 1", url: "/item1"},
|
||||
{title: "item 2", url: "/item2"},
|
||||
{title: "item 3", url: "/item3"}
|
||||
];
|
||||
|
||||
m.render(document.body, [
|
||||
m("ul.nav",
|
||||
links.map(function(link) {
|
||||
return m("li",
|
||||
m("a", {href: link.url}, link.title)
|
||||
);
|
||||
})
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
yields:
|
||||
|
||||
```markup
|
||||
<body>
|
||||
<ul class="nav">
|
||||
<li><a href="/item1">item 1</a></li>
|
||||
<li><a href="/item2">item 2</a></li>
|
||||
<li><a href="/item3">item 3</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
```
|
||||
|
||||
As you can see, flow control is done with vanilla Javascript. This allows the developer to abstract away any aspect of the template at will.
|
||||
|
||||
---
|
||||
|
||||
Note that you can use both Javascript property names and HTML attribute names to set values in the `attributes` argument, but you should pass a value of appropriate type. If an attribute has the same name in Javascript and in HTML, then Mithril assumes you're setting the Javascript property.
|
||||
|
||||
```javascript
|
||||
m("div", {class: "widget"}); //yields <div class="widget"></div>
|
||||
|
||||
m("div", {className: "widget"}); //yields <div class="widget"></div>
|
||||
|
||||
m("button", {onclick: alert}); //yields <button></button>, which alerts its event argument when clicked
|
||||
|
||||
//note this uses the Javascript syntax (uppercase "O") for `readonly`
|
||||
//in order to set the boolean javascript property instead of the HTML attribute
|
||||
m("input", {readOnly: true}); //yields <input readonly />
|
||||
|
||||
//using the HTML attribute name will call `setAttribute`, which may not be what you want
|
||||
m("input", {readonly: false}); //yields <input readonly="false" />, which is still readonly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Note that you can use JSON syntax if the attribute name you are setting has non-alphanumeric characters:
|
||||
|
||||
```javascript
|
||||
m("div", {"data-index": 1}); //yields <div data-index="1"></div>
|
||||
```
|
||||
|
||||
You can set inline styles like this:
|
||||
|
||||
```javascript
|
||||
m("div", {style: {border: "1px solid red"}}); //yields <div style="border:1px solid red;"></div>
|
||||
```
|
||||
|
||||
Note that in order to keep the framework lean, Mithril does not auto-append units like `px` or `%` to any values. Typically, you should not even be using inline styles to begin with (unless you are dynamically changing them).
|
||||
|
||||
Mithril also does not auto-camel-case CSS properties on inline style attributes, so you should use the Javascript syntax when setting them via Javascript objects:
|
||||
|
||||
```javascript
|
||||
m("div", {style: {textAlign: "center"}}); //yields <div style="text-align:center;"></div>
|
||||
m("div", {style: {cssFloat: "left"}}); //yields <div style="float:left;"></div>
|
||||
|
||||
//this does not work
|
||||
m("div", {style: {"text-align": "center"}});
|
||||
m("div", {style: {float: "left"}});
|
||||
```
|
||||
|
||||
You can find the [Javascript syntax for all the CSS rules here](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference).
|
||||
|
||||
You can, however, use CSS syntax when defining style rules as inline strings:
|
||||
|
||||
```javascript
|
||||
m("div[style='text-align:center']"); //yields <div style="text-align:center;"></div>
|
||||
```
|
||||
|
||||
One caveat of using the CSS syntax is that it clobbers the `style` attribute in the DOM element on redraws, so this syntax is not appropriate if you need to use it in conjunction with 3rd party tools that modify the element's style outside of Mithril's templates (e.g. via `config`, which is explained below)
|
||||
|
||||
---
|
||||
|
||||
### Binding to data
|
||||
|
||||
In order to stay flexible, Mithril doesn't provide helpers for bi-directional bindings out of the box. However, bindings can be implemented easily:
|
||||
|
||||
```javascript
|
||||
//a data store
|
||||
var name = m.prop("")
|
||||
|
||||
//binding the data store in a view
|
||||
m("input", {oninput: m.withAttr("value", name), value: name()})
|
||||
```
|
||||
|
||||
In the code above, the `oninput` event handler updates the `name` getter-setter, and the Mithril auto-redrawing system redraws the template in order to update the displayed value. You can read more about the [`m.prop` getter-setter utility here](mithril.prop.md) and the [`m.withAttr` event handler factory here](mithril.withAttr.md). You can also [learn how the redrawing system works here](auto-redrawing.md).
|
||||
|
||||
Note that Mithril always considers the model layer data to be canonical. This means that in the code below, the input on screen will overwritten by the model data any time a redraw happens:
|
||||
|
||||
```javascript
|
||||
//note that we are not updating the value of the `name` getter-setter via an event handler
|
||||
//redraws will always overwrite the current UI value with the value of `name()`
|
||||
m("input", {value: name()})
|
||||
```
|
||||
|
||||
Expressiveness can be achieved using standard refactoring techniques:
|
||||
|
||||
```javascript
|
||||
//refactor the binding to a simple helper
|
||||
var binds = function(prop) {
|
||||
return {oninput: m.withAttr("value", prop), value: prop()}
|
||||
}
|
||||
|
||||
//a data store
|
||||
var name = m.prop("")
|
||||
|
||||
//binding the data store in a view
|
||||
m("input", binds(name))
|
||||
```
|
||||
|
||||
Here's an example of a more aggressive refactor:
|
||||
|
||||
```javascript
|
||||
//refactor the binding to a simple helper
|
||||
var input = function(prop) {
|
||||
return m("input", {oninput: m.withAttr("value", prop), value: prop()})
|
||||
}
|
||||
|
||||
//a data store
|
||||
var name = m.prop("")
|
||||
|
||||
//binding the data store in a view
|
||||
input(name)
|
||||
```
|
||||
|
||||
Alternatively, you can also explore other techniques in order to achieve better [performance](http://lhorie.github.io/mithril-blog/asymmetrical-data-bindings.html) and [expressiveness](http://lhorie.github.io/mithril-blog/extending-the-view-language.html).
|
||||
|
||||
---
|
||||
|
||||
### Using HTML entities
|
||||
|
||||
By default, Mithril escapes HTML strings in order to help prevent XSS attacks.
|
||||
|
||||
```javascript
|
||||
m("div", "×") //becomes <div>&times;</div>
|
||||
```
|
||||
|
||||
You can unescape trusted HTML strings by using [`m.trust`](mithril.trust.md)
|
||||
|
||||
```javascript
|
||||
m("div", m.trust("×")) //becomes <div>×</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Accessing the real DOM element
|
||||
|
||||
You can define a non-HTML-standard attribute called `config`. This special parameter allows you to call methods on the DOM element after it gets created.
|
||||
|
||||
This is useful, for example, if you declare a `canvas` element and want to use the Javascript API to draw:
|
||||
|
||||
```javascript
|
||||
function draw(element, isInitialized, context) {
|
||||
//don't redraw if we did once already
|
||||
if (isInitialized) return;
|
||||
|
||||
var ctx = element.getContext("2d");
|
||||
/* draws stuff */
|
||||
}
|
||||
|
||||
var view = [
|
||||
m("canvas", {config: draw})
|
||||
]
|
||||
|
||||
//this creates the canvas element, and therefore, `isInitialized` is false
|
||||
m.render(document.body, view);
|
||||
|
||||
//here, isInitialized is `true`
|
||||
m.render(document.body, view);
|
||||
```
|
||||
|
||||
One common way of using `config` is in conjunction with [`m.route`](mithril.route.md), which is an unobtrusive extension to links that allow Mithril's routing system to work transparently regardless of which routing mode is used.
|
||||
|
||||
```javascript
|
||||
//this link can use any of Mithril's routing system modes
|
||||
//(i.e. it can use either the hash, the querystring or the pathname as the router implementation)
|
||||
//without needing to hard-code any syntax (`#` or `?`) in the `href` attribute.
|
||||
m("a[href='/dashboard']", {config: m.route}, "Dashboard");
|
||||
```
|
||||
|
||||
The `config` mechanism can also be used to put focus on form inputs, and call methods that would not be possible to execute via the regular attribute syntax.
|
||||
|
||||
Also note that the `config` callback only runs after a rendering lifecycle is done. Therefore, you should not use `config` to modify controller and model values, if you expect these changes to render immediately. Changes to controller and model values in this fashion will only render on the next `m.render` or `m.mount` call.
|
||||
|
||||
You can use this mechanism to attach custom event listeners to controller methods (for example, when integrating with third party libraries), but you are responsible for making sure the integration with Mithril's autoredrawing system is in place. See the [integration guide](integration.md) for more information.
|
||||
|
||||
You can also use it to attach events to other elements (for example, `window.onresize`), but you should remove such event handlers via `ctx.onunload` to avoid surprises.
|
||||
|
||||
---
|
||||
|
||||
#### Persisting config data
|
||||
|
||||
The third argument for `config` allows you to map data to a virtual DOM element in a way that persists across redraws. This is useful when a `config` instantiates 3rd party classes and accesses the instance on redraws.
|
||||
|
||||
The example below shows a contrived redraw counter. In it, the count is stored in the context object and re-accessed on each redraw.
|
||||
|
||||
```javascript
|
||||
function alertsRedrawCount(element, isInit, context) {
|
||||
if (!isInit) context.count = 0
|
||||
alert(++context.count)
|
||||
}
|
||||
|
||||
m("div", {config: alertsRedrawCount})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Destructors
|
||||
|
||||
If the `context` object that is passed to a `config` function has a property called `onunload`, this function will be called when the element gets detached from the document by Mithril's diff engine.
|
||||
|
||||
This is useful if there are cleanup tasks that need to be run when an element is destroyed (e.g. clearing `setTimeout`'s, etc)
|
||||
|
||||
```javascript
|
||||
function unloadable(element, isInit, context) {
|
||||
context.timer = setTimeout(function() {
|
||||
alert("timed out!");
|
||||
}, 1000);
|
||||
|
||||
context.onunload = function() {
|
||||
clearTimeout(context.timer);
|
||||
console.log("unloaded the div");
|
||||
}
|
||||
};
|
||||
|
||||
m.render(document, m("div", {config: unloadable}));
|
||||
|
||||
m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets called
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Persisting DOM elements across route changes
|
||||
|
||||
When using the [router](mithril.route.md), a route change recreates the DOM tree from scratch in order to unload plugins from the previous page. If you want to keep a DOM element intact across a route change, you can set the `retain` flag in the config's context object.
|
||||
|
||||
In the example below, there are two routes, each of which loads a component when a user navigates to their respective URLs. Both components use a `menu` template, which contains links for navigation between the two components, and an expensive-to-reinitialize element. Setting `context.retain = true` in the element's config function allows the span to stay intact after a route change.
|
||||
|
||||
```javascript
|
||||
//a menu template
|
||||
var menu = function() {
|
||||
return m("div", [
|
||||
m("a[href='/']", {config: m.route}, "Home"),
|
||||
m("a[href='/contact']", {config: m.route}, "Contact"),
|
||||
//an expensive-to-initialize DOM element
|
||||
m("span", {config: persistent})
|
||||
])
|
||||
}
|
||||
//a configuration that persists across route changes
|
||||
function persistent(el, isInit, context) {
|
||||
context.retain = true
|
||||
|
||||
if (!isInit) {
|
||||
//only runs once, even if you move back and forth between `/` and `/contact`
|
||||
doSomethingExpensive(el)
|
||||
}
|
||||
}
|
||||
|
||||
//components that use the menu above
|
||||
var Home = {
|
||||
controller: function() {},
|
||||
view: function() {
|
||||
return m("div", [
|
||||
menu(),
|
||||
m("h1", "Home")
|
||||
])
|
||||
}
|
||||
}
|
||||
var Contact = {
|
||||
view: function() {
|
||||
return m("div", [
|
||||
menu(),
|
||||
m("h2", "Contact")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": Home,
|
||||
"/contact": Contact
|
||||
})
|
||||
```
|
||||
|
||||
Note that even if you set `context.retain = true`, the element will still be destroyed and recreated if it is different enough from the existing element. An element is considered "different enough" if:
|
||||
|
||||
- the tag name changes, or
|
||||
- the list of HTML attributes changes, or
|
||||
- the value of the element's id attribute changes
|
||||
|
||||
In addition, setting `context.retain = false` will also cause the element to be recreated, even if it is not considered different enough.
|
||||
|
||||
---
|
||||
|
||||
#### SVG
|
||||
|
||||
You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively).
|
||||
|
||||
Mithril automatically figures out the correct XML namespaces when it sees an SVG island in the virtual DOM tree.
|
||||
|
||||
```javascript
|
||||
m("svg[height='200px'][width='200px']", [
|
||||
m("image[href='foo.jpg'][height='200px'][width='200px']")
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Dealing with focus
|
||||
|
||||
The virtual DOM diffing algorithm has a weakness: a naive diff is not aware of the identity of DOM elements. In practice, this means performing operations like shifting an item from the beginning of a list would cause every element in the list to be diffed and potentially recreated. Another side-effect is that UI state like input focus is not tracked correctly if the focused element moves around, and likewise, state for 3rd party plugins that are added via `config` can also end up in the wrong element.
|
||||
|
||||
Fortunately, with Mithril, it's possible for developers to attach an identity key to elements so that array operations like shift, splice and sort only affect the minimum amount of elements required, leaving the rest of the DOM elements untouched when a redraw happens. This allows us to maintain input focus and plugin state correctly.
|
||||
|
||||
To maintain the identities of DOM elements, you need to add a `key` property to the direct children of the array that you're planning to modify. The key for each child must be unique among a list of sibling DOM elements, but it does not need to be globally unique. Also, keys must be either strings or numbers.
|
||||
|
||||
```javascript
|
||||
m("ul", [
|
||||
items.map(function(item) {
|
||||
return m("li", {key: item.id}, [
|
||||
m("input")
|
||||
]);
|
||||
})
|
||||
]);
|
||||
```
|
||||
|
||||
In the example above, input focus would be maintained correctly after a redraw even if `items` got sorted or reversed. The key is defined in the `li`, which is the closest element to the `items` array, not directly on the `input`, even though we want to track focus on the input.
|
||||
|
||||
Note that in addition to the presence of the `key` attribute, diffing rules also apply in determining whether an element is recreated. Elements are recreated if either their node name changes, or if the list of attribute names change, or if the ID attribute changes. To avoid surprises, be sure to change only attribute values, using `undefined` or `null` as values if appropriate, rather than conditionally substituting attribute dictionaries altogether.
|
||||
|
||||
```javascript
|
||||
//avoid using this idiom
|
||||
m("li", selected ? {class: "active"} : {})
|
||||
|
||||
//use this idiom instead
|
||||
m("li", {class: selected ? "active" : ""})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dealing with sorting and deleting in lists
|
||||
|
||||
As with input focus, we can maintain referential integrity between data in a list and the respective DOM representation by using keys.
|
||||
|
||||
```javascript
|
||||
m("ul", [
|
||||
items.map(function(item) {
|
||||
return m("li", {key: item.id}, [
|
||||
m("input")
|
||||
]);
|
||||
})
|
||||
]);
|
||||
```
|
||||
|
||||
You should always use keys if you need to sort lists, remove items from them or splice them in any way.
|
||||
|
||||
---
|
||||
|
||||
### Component Shorthand
|
||||
|
||||
If the first argument to `m()` is a component, it acts as an alias of `m.component()`
|
||||
|
||||
```javascript
|
||||
var MyComponent = {
|
||||
controller: function() {
|
||||
return {greeting: "hello"}
|
||||
},
|
||||
view: function(ctrl, args) {
|
||||
return m("h1", ctrl.greeting + " " + args.data)
|
||||
}
|
||||
}
|
||||
|
||||
m.render(document.body, [
|
||||
//the two lines below are equivalent
|
||||
m(MyComponent, {data: "world"}),
|
||||
m.component(MyComponent, {data: "world"})
|
||||
])
|
||||
```
|
||||
|
||||
See [components](mithril.component.md) for more information.
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
[How to read signatures](how-to-read-signatures.md)
|
||||
|
||||
```clike
|
||||
VirtualElement m(String selector [, Attributes attributes] [, Children... children])
|
||||
|
||||
where:
|
||||
VirtualElement :: Object { String tag, Attributes attributes, Children children }
|
||||
Attributes :: Object<any | void config(DOMElement element, Boolean isInitialized, Object context, VirtualElement vdom)>
|
||||
Children :: String text | VirtualElement virtualElement | Component | SubtreeDirective directive | Array<Children children>
|
||||
Component :: Object { Function? controller, Function view }
|
||||
SubtreeDirective :: Object { String subtree }
|
||||
```
|
||||
|
||||
- **String selector**
|
||||
|
||||
This string should be a CSS rule that represents a DOM element.
|
||||
|
||||
Only tag, id, class and attribute selectors are supported.
|
||||
|
||||
If the tag selector is omitted, it defaults to `div`.
|
||||
|
||||
Note that if the same attribute is defined in the both `selector` and `attributes` parameters, the value in `attributes` is used.
|
||||
|
||||
For developer convenience, Mithril makes an exception for the `class` attribute: if there are classes defined in both parameters, they are concatenated as a space separated list. It does not, however, de-dupe classes if the same class is declared twice.
|
||||
|
||||
*Examples:*
|
||||
|
||||
`"div"`
|
||||
|
||||
`"#container"`
|
||||
|
||||
`".active"`
|
||||
|
||||
`"[title='Application']"`
|
||||
|
||||
`"div#container.active[title='Application']"`
|
||||
|
||||
`".active#container"`
|
||||
|
||||
- **Attributes attributes** (optional)
|
||||
|
||||
This key-value map should define a list of HTML attributes and their respective values.
|
||||
|
||||
You can use both HTML and Javascript attribute names. For example, both `class` and `className` are valid.
|
||||
|
||||
Values' types should match the expected type for the respective attribute.
|
||||
|
||||
For example, the value for `className` should be a string.
|
||||
|
||||
When a attribute name expects different types for the value in HTML and Javascript, the Javascript type should be used.
|
||||
|
||||
For example, the value for the `onclick` attribute should be a function.
|
||||
|
||||
Similar, setting the value of attribute `readonly` to `false` is equivalent to removing the attribute in HTML.
|
||||
|
||||
It's also possible to set values to Javascript-only properties, such as `hash` in a `<a>` element.
|
||||
|
||||
Note that if the same attribute is defined in the both `selector` and `attributes` parameters, the value in `attributes` is used.
|
||||
|
||||
For developer convenience, Mithril makes an exception for the `class` attribute: if there are classes defined in both parameters, they are concatenated as a space separated list. It does not, however, de-dupe classes if the same class is declared twice.
|
||||
|
||||
*Examples:*
|
||||
|
||||
`{ title: "Application" }`
|
||||
|
||||
`{ onclick: function(e) { /*do stuff*/ } }`
|
||||
|
||||
`{ style: {border: "1px solid red"} }`
|
||||
|
||||
- #### The `config` attribute
|
||||
|
||||
**void config(DOMElement element, Boolean isInitialized, Object context, VirtualElement vdom)** (optional)
|
||||
|
||||
You can define a non-HTML-standard attribute called `config`. This special parameter allows you to call methods on the DOM element after it gets created.
|
||||
|
||||
This is useful, for example, if you declare a `canvas` element and want to use the Javascript API to draw:
|
||||
|
||||
```javascript
|
||||
function draw(element, isInitialized) {
|
||||
//don't redraw if we did once already
|
||||
if (isInitialized) return;
|
||||
|
||||
var ctx = element.getContext("2d");
|
||||
/* draws stuff */
|
||||
}
|
||||
|
||||
var view = [
|
||||
m("canvas", {config: draw})
|
||||
]
|
||||
|
||||
//this creates the canvas element, and therefore, `isInitialized` is false
|
||||
m.render(document.body, view);
|
||||
|
||||
//here, isInitialized is `true`
|
||||
m.render(document.body, view);
|
||||
```
|
||||
|
||||
One common way of using `config` is in conjunction with [`m.route`](mithril.route.md), which is an unobtrusive extension to links that allow Mithril's routing system to work transparently regardless of which routing mode is used.
|
||||
|
||||
```javascript
|
||||
//this link can use any of Mithril's routing system modes
|
||||
//(i.e. it can use either the hash, the querystring or the pathname as the router implementation)
|
||||
//without needing to hard-code any syntax (`#` or `?`) in the `href` attribute.
|
||||
m("a[href='/dashboard']", {config: m.route}, "Dashboard");
|
||||
```
|
||||
|
||||
The `config` mechanism can also be used to put focus on form inputs, and call methods that would not be possible to execute via the regular attribute syntax.
|
||||
|
||||
Also note that the `config` callback only runs after a rendering lifecycle is done. Therefore, you should not use `config` to modify controller and model values, if you expect these changes to render immediately. Changes to controller and model values in this fashion will only render on the next `m.render` or `m.mount` call.
|
||||
|
||||
You can use this mechanism to attach custom event listeners to controller methods (for example, when integrating with third party libraries), but you are responsible for making sure the integration with Mithril's autoredrawing system is in place. See the [integration guide](integration.md) for more information.
|
||||
|
||||
You can also use it to attach events to other elements (for example, `window.onresize`), but you should remove such event handlers via `ctx.onunload` to avoid surprises.
|
||||
|
||||
- **DOMElement element**
|
||||
|
||||
The DOM element that corresponds to virtual element defined by the `m()` call.
|
||||
|
||||
- **Boolean isInitialized**
|
||||
|
||||
Whether this is the first time we are running this function on this element. This flag is false the first time it runs on an element, and true on redraws that happen after the element has been created.
|
||||
|
||||
- **Object context**
|
||||
|
||||
An object that retains its state across redraws. It can be used to store instances of 3rd party classes that need to be accessed more than one time throughout the lifecycle of a page.
|
||||
|
||||
The example below shows a contrived redraw counter. In it, the count is stored in the context object and re-accessed on each redraw.
|
||||
|
||||
```javascript
|
||||
function alertsRedrawCount(element, isInit, context) {
|
||||
if (!isInit) context.count = 0
|
||||
alert(++context.count)
|
||||
}
|
||||
|
||||
m("div", {config: alertsRedrawCount})
|
||||
```
|
||||
|
||||
If the `context` object that is passed to a `config` function has a property called `onunload`, this function will be called when the element gets detached from the document by Mithril's diff engine.
|
||||
|
||||
This is useful if there are cleanup tasks that need to be run when an element is destroyed (e.g. clearing `setTimeout`'s, etc)
|
||||
|
||||
```javascript
|
||||
function unloadable(element, isInit, context) {
|
||||
context.timer = setTimeout(function() {
|
||||
alert("timed out!");
|
||||
}, 1000);
|
||||
|
||||
context.onunload = function() {
|
||||
clearTimeout(context.timer);
|
||||
console.log("unloaded the div");
|
||||
}
|
||||
};
|
||||
|
||||
m.render(document, m("div", {config: unloadable}));
|
||||
|
||||
m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets called
|
||||
```
|
||||
|
||||
- **VirtualElement vdom**
|
||||
|
||||
The virtual DOM element to which the `config` function is attached
|
||||
|
||||
- **Children children** (optional)
|
||||
|
||||
If this argument is a string, it will be rendered as a text node. To render a string as HTML, see [`m.trust`](mithril.trust.md)
|
||||
|
||||
If it's a VirtualElement, it will be rendered as a DOM Element.
|
||||
|
||||
If it's a [component](mithril.component.md), the component will be instantiated and managed internally by Mithril
|
||||
|
||||
If it's a list, its contents will recursively be rendered as appropriate and appended as children of the element being created.
|
||||
|
||||
If it's a SubtreeDirective with the value "retain", it will retain the existing DOM tree in place, if any. See [subtree directives.md](mithril.render.md#subtree-directives) for more information.
|
||||
|
||||
- **returns** VirtualElement
|
||||
|
||||
The returned VirtualElement is a Javascript data structure that represents the DOM element to be rendered by [`m.render`](mithril.render.md)
|
||||
|
|
@ -1,697 +0,0 @@
|
|||
## m.request
|
||||
|
||||
---
|
||||
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Processing-web-service-data](#processing-web-service-data)
|
||||
- [Bind redirection code](#bind-redirection-code)
|
||||
- [Binding errors](#binding-errors)
|
||||
- [Queuing operations](#queuing-operations)
|
||||
- [Casting the Response Data to a Class](#casting-the-response-data-to-a-class)
|
||||
- [Unwrapping Response Data](#unwrapping-response-data)
|
||||
- [Using Different Data Transfer Formats](#using-different-data-transfer-formats)
|
||||
- [File uploads with FormData](#file-uploads-with-formdata)
|
||||
- [Using variable data formats](#using-variable-data-formats)
|
||||
- [Extracting Metadata from the Response](#extracting-metadata-from-the-response)
|
||||
- [Custom request rejections](#custom-request-rejections)
|
||||
- [Setting headers](#setting-headers)
|
||||
- [Configuring the underlying XMLHttpRequest](#configuring-the-underlying-xmlhttprequest)
|
||||
- [Aborting a request](#aborting-a-request)
|
||||
- [Using JSON-P](#using-json-p)
|
||||
- [Rendering before web service requests finish](#rendering-before-web-service-requests-finish)
|
||||
- [Signature](#signature)
|
||||
|
||||
---
|
||||
|
||||
This is a high-level utility for working with web services, which allows writing asynchronous code relatively procedurally.
|
||||
|
||||
By default, it assumes server responses are in JSON format and optionally instantiates a class with the response data.
|
||||
|
||||
It provides a number of useful features out of the box:
|
||||
|
||||
- The ability to get an early reference to a container that will hold the asynchronous response
|
||||
- The ability to queue operations to be performed after the asynchronous request completes
|
||||
- The ability to "cast" the response to a class of your choice
|
||||
- The ability to unwrap data in a response that includes metadata properties
|
||||
|
||||
---
|
||||
|
||||
### Basic usage
|
||||
|
||||
The basic usage pattern for `m.request` returns an [`m.prop`](mithril.prop.md) getter-setter, which is populated when the AJAX request completes.
|
||||
|
||||
The returned getter-setter can be thought of as a box: you can pass this reference around cheaply, and you can "unwrap" its value when needed.
|
||||
|
||||
```javascript
|
||||
var users = m.request({method: "GET", url: "/user"});
|
||||
|
||||
//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]`
|
||||
//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users
|
||||
//i.e. users() //[{name: "John"}, {name: "Mary"}]
|
||||
```
|
||||
|
||||
Note that this getter-setter holds an *undefined* value until the AJAX request completes. Attempting to unwrap its value early will likely result in errors.
|
||||
|
||||
The returned getter-setter also implements the [promise](mithril.deferred.md) interface (also known as a *thennable*): this is the mechanism you should always use to queue operations to be performed on the data from the web service.
|
||||
|
||||
The simplest use case of this feature is to implement functional value assignment via `m.prop` (i.e. the same thing as above). You can bind a pre-existing getter-setter by passing it in as a parameter to a `.then` method:
|
||||
|
||||
```javascript
|
||||
var users = m.prop([]); //default value
|
||||
|
||||
m.request({method: "GET", url: "/user"}).then(users)
|
||||
//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]`
|
||||
//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users
|
||||
//i.e. users() //[{name: "John"}, {name: "Mary"}]
|
||||
```
|
||||
|
||||
This syntax allows you to bind intermediate results before piping them down for further processing, for example:
|
||||
|
||||
```javascript
|
||||
var users = m.prop([]); //default value
|
||||
var doSomething = function() { /*...*/ }
|
||||
|
||||
m.request({method: "GET", url: "/user"}).then(users).then(doSomething)
|
||||
```
|
||||
|
||||
While both basic assignment syntax and thennable syntax can be used to the same effect, typically it's recommended that you use the assignment syntax whenever possible, as it's easier to read.
|
||||
|
||||
The thennable mechanism is intended to be used in three ways:
|
||||
|
||||
- in the model layer: to process web service data in transformative ways (e.g. filtering a list based on a parameter that the web service doesn't support)
|
||||
- in the controller layer: to bind redirection code upon a condition
|
||||
- in the controller layer: to bind error messages
|
||||
|
||||
#### Processing web service data
|
||||
|
||||
This step is meant to be done in the model layer. Doing it in the controller level is also possible, but philosophically not recommended, because by tying logic to a controller, the code becomes harder to reuse due to unrelated controller dependencies.
|
||||
|
||||
In the example below, the `listEven` method returns a getter-setter that resolves to a list of users containing only users whose id is even.
|
||||
|
||||
```javascript
|
||||
//model
|
||||
var User = {}
|
||||
|
||||
User.listEven = function() {
|
||||
return m.request({method: "GET", url: "/user"}).then(function(list) {
|
||||
return list.filter(function(user) {return user.id % 2 == 0});
|
||||
});
|
||||
}
|
||||
|
||||
//controller
|
||||
var controller = function() {
|
||||
return {users: User.listEven()}
|
||||
}
|
||||
```
|
||||
|
||||
#### Bind redirection code
|
||||
|
||||
This step is meant to be done in the controller layer. Doing it in the model level is also possible, but philosophically not recommended, because by tying redirection to the model, the code becomes harder to reuse due to overly tight coupling.
|
||||
|
||||
In the example below, we use the previously defined `listEven` model method and queue a controller-level function that redirects to another page if the user list is empty.
|
||||
|
||||
```javascript
|
||||
//controller
|
||||
var controller = function() {
|
||||
return {
|
||||
users: User.listEven().then(function(users) {
|
||||
if (users.length == 0) m.route("/add");
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Binding errors
|
||||
|
||||
Mithril thennables take two functions as optional parameters: the first parameter is called if the web service request completes successfully. The second one is called if it completes with an error.
|
||||
|
||||
Error binding is meant to be done in the controller layer. Doing it in the model level is also possible, but generally leads to more code in order to connect all the dots.
|
||||
|
||||
In the example below, we bind an error getter-setter to our previous controller so that the `error` variable gets populated if the server throws an error.
|
||||
|
||||
```javascript
|
||||
//controller
|
||||
var controller = function() {
|
||||
this.error = m.prop("")
|
||||
|
||||
this.users = User.listEven().then(function(users) {
|
||||
if (users.length == 0) m.route("/add");
|
||||
}, this.error)
|
||||
}
|
||||
```
|
||||
|
||||
If the controller doesn't already have a success callback to run after a request resolves, you can still bind errors like this:
|
||||
|
||||
```javascript
|
||||
//controller
|
||||
var controller = function() {
|
||||
this.error = m.prop("")
|
||||
|
||||
this.users = User.listEven().then(null, this.error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Queuing Operations
|
||||
|
||||
As you saw, you can chain operations that act on the response data. Typically this is required in three situations:
|
||||
|
||||
- in model-level methods if client-side processing is needed to make the data useful for a controller or view.
|
||||
- in the controller, to redirect after a model service resolves.
|
||||
- in the controller, to bind error messages
|
||||
|
||||
In the example below, we take advantage of queuing to debug the AJAX response data prior to doing further processing on the user list
|
||||
|
||||
```javascript
|
||||
//a FP-friendly console.log
|
||||
var log = function(value) {
|
||||
console.log(value)
|
||||
return value
|
||||
}
|
||||
|
||||
var users = m.request({method: "GET", url: "/user"})
|
||||
.then(log)
|
||||
.then(function(users) {
|
||||
//add one more user to the response
|
||||
return users.concat({name: "Jane"})
|
||||
})
|
||||
|
||||
//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]`
|
||||
//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users
|
||||
//i.e. users() //[{name: "John"}, {name: "Mary"}, {name: "Jane"}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Casting the Response Data to a Class
|
||||
|
||||
It's possible to auto-cast a JSON response to a class. This is useful when we want to control access to certain properties in an object, as opposed to exposing all the fields in POJOs (plain old Javascript objects) for arbitrary processing.
|
||||
|
||||
In the example below, `User.list` returns a list of `User` instances.
|
||||
|
||||
```javascript
|
||||
var User = function(data) {
|
||||
this.name = m.prop(data.name);
|
||||
}
|
||||
|
||||
User.list = function() {
|
||||
return m.request({method: "GET", url: "/user", type: User});
|
||||
}
|
||||
|
||||
var users = User.list();
|
||||
//assuming the response contains the following data: `[{name: "John"}, {name: "Mary"}]`
|
||||
//then when resolved (e.g. in a view), `users` will contain a list of User instances
|
||||
//i.e. users()[0].name() == "John"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Unwrapping Response Data
|
||||
|
||||
Often, web services return the relevant data wrapped in objects that contain metadata.
|
||||
|
||||
Mithril allows you to unwrap the relevant data, by providing two callback hooks: `unwrapSuccess` and `unwrapError`.
|
||||
|
||||
These hooks allow you to unwrap different parts of the response data depending on whether it succeed or failed.
|
||||
|
||||
```javascript
|
||||
var users = m.request({
|
||||
method: "GET",
|
||||
url: "/user",
|
||||
unwrapSuccess: function(response) {
|
||||
return response.data;
|
||||
},
|
||||
unwrapError: function(response) {
|
||||
return response.error;
|
||||
}
|
||||
});
|
||||
|
||||
//assuming the response is: `{data: [{name: "John"}, {name: "Mary"}], count: 2}`
|
||||
//then when resolved (e.g. in a view), the `users` getter-setter will contain a list of users
|
||||
//i.e. users() //[{name: "John"}, {name: "Mary"}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Using Different Data Transfer Formats
|
||||
|
||||
By default, `m.request` uses JSON to send and receive data to web services. You can override this by providing `serialize` and `deserialize` options:
|
||||
|
||||
```javascript
|
||||
var users = m.request({
|
||||
method: "GET",
|
||||
url: "/user",
|
||||
serialize: mySerializer,
|
||||
deserialize: myDeserializer
|
||||
});
|
||||
```
|
||||
|
||||
One typical way to override this is to receive as-is responses. The example below shows how to receive a plain string from a txt file.
|
||||
|
||||
```javascript
|
||||
var file = m.request({
|
||||
method: "GET",
|
||||
url: "myfile.txt",
|
||||
deserialize: function(value) {return value;}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### File uploads with FormData
|
||||
|
||||
To use the HTML5 FormData object as the payload for a request, you need to override the `serialize` option. By default, `serialize` converts an object to JSON, but in the case of a FormData payload, you want to pass the object intact.
|
||||
|
||||
```javascript
|
||||
//assume the file comes from an HTML5 drag-n-drop event
|
||||
var file = e.dataTransfer.files[0]
|
||||
|
||||
var data = new FormData();
|
||||
data.append("file", file)
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/upload",
|
||||
data: data,
|
||||
serialize: function(data) {return data}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Using variable data formats
|
||||
|
||||
By default, Mithril assumes both success and error responses are in JSON format, but some servers may not return JSON responses when returning HTTP error codes (e.g. 404)
|
||||
|
||||
You can get around this issue by using `extract`
|
||||
|
||||
```javascript
|
||||
var nonJsonErrors = function(xhr) {
|
||||
return xhr.status > 200 ? JSON.stringify(xhr.responseText) : xhr.responseText
|
||||
}
|
||||
|
||||
m.request({method: "GET", url: "/foo/bar.x", extract: nonJsonErrors})
|
||||
.then(function(data) {}, function(error) {console.log(error)})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Extracting Metadata from the Response
|
||||
|
||||
The `extract` method can be used to read metadata from HTTP response headers or the status field of an XMLHttpRequest.
|
||||
|
||||
```javascript
|
||||
var extract = function(xhr, xhrOptions) {
|
||||
if (xhrOptions.method == "HEAD") return xhr.getResponseHeader("x-item-count")
|
||||
else return xhr.responseText
|
||||
}
|
||||
|
||||
m.request({method: "POST", url: "/foo", extract: extract});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Custom request rejections
|
||||
|
||||
If you want to be able to handle a condition as an error in a promise rejection handler, you can throw an `Error` from `extract` to reject the promise.
|
||||
|
||||
This is useful, for example, if you received invalid JSON from the server in production and you want to display a message to the user saying that the server is offline.
|
||||
|
||||
```javascript
|
||||
var extract = function(xhr, xhrOptions) {
|
||||
try {
|
||||
return JSON.stringify(xhr.responseText)
|
||||
}
|
||||
catch (e) {
|
||||
//e instanceof SyntaxError == true
|
||||
//by default `e` would be caught by Mithril's promise exception monitor and rethrown to the console
|
||||
//this new error follows Promises/A+ specifications and triggers a rejection in the downstream promises without hitting the console.
|
||||
throw new Error("Server is offline")
|
||||
}
|
||||
}
|
||||
|
||||
m.request({method: "POST", url: "/foo", extract: extract});
|
||||
```
|
||||
|
||||
You can read more about the [promise exception monitor here](mithril.deferred.md#unchecked-error-handling).
|
||||
|
||||
---
|
||||
|
||||
### Setting headers
|
||||
|
||||
The `headers` option can be used to add or modify existing headers. The example The example below shows how to configure a `POST` request where the server expects requests to have a `Content-Type: application/json` header.
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "/foo",
|
||||
headers: {"Content-Type": "application/json"}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Configuring the underlying XMLHttpRequest
|
||||
|
||||
The `config` option can be used to arbitrarily configure the native XMLHttpRequest instance and to access properties that would not be accessible otherwise. For example, this is how to listen for progress notifications.
|
||||
|
||||
```javascript
|
||||
var xhrConfig = function(xhr) {
|
||||
xhr.onprogress = function(ev) {
|
||||
ev = ev || event;
|
||||
if (ev.lengthComputable) {
|
||||
console.log(ev.loaded + " bytes sent out of " + ev.total + " total.")
|
||||
} else {
|
||||
console.log(ev.loaded + " bytes sent.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.request({method: "POST", url: "/foo", config: xhrConfig});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Aborting a request
|
||||
|
||||
The `config` option can also be used to retrieve the `XMLHttpRequest` instance for aborting the request. This idiom can also be used to attach `onprogress` event handlers.
|
||||
|
||||
```javascript
|
||||
var transport = m.prop();
|
||||
|
||||
m.request({method: "POST", url: "/foo", config: transport});
|
||||
|
||||
//the `transport` getter-setter contains an instance of XMLHttpRequest
|
||||
transport().abort();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Using JSON-P
|
||||
|
||||
To make JSON-P requests, add the `dataType` option instead of `method`. You should not add the `callback` querystring parameter; Mithril already does that internally.
|
||||
|
||||
```javascript
|
||||
m.request({dataType: "jsonp", url: "/api/User"});
|
||||
```
|
||||
|
||||
Some services (e.g. Flickr) don't follow the convention of calling the `callback` parameter `callback`. In order to specify the name of the querystring parameter that indicates the callback function, use the `callbackKey` option:
|
||||
|
||||
```javascript
|
||||
m.request({
|
||||
dataType: "jsonp",
|
||||
callbackKey: "jsoncallback",
|
||||
url: "http://api.flickr.com/services/feeds/photos_public.gne?tags=monkey&tagmode=any&format=json"
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rendering before web service requests finish
|
||||
|
||||
By default, Mithril waits for web service requests to complete before attempting a redraw. This ensures that data being accessed in the view isn't nullable as a result of asynchronous data not being available yet.
|
||||
|
||||
However, sometimes we do want to be able to redraw before a web service request completes, either because one web service out of many is slow, or because we don't need its response in order to redraw.
|
||||
|
||||
Setting the `background` option to `true` prevents a request from affecting redrawing. This means it's possible for a view to attempt to use data before it is available. You can specify an initial value for the `m.request` getter-setter in order to avoid having to write defensive code against potential null reference exceptions:
|
||||
|
||||
```javascript
|
||||
var demo = {}
|
||||
|
||||
demo.controller = function() {
|
||||
return {
|
||||
users: m.request({method: "GET", url: "/api/user", background: true, initialValue: []})
|
||||
}
|
||||
}
|
||||
|
||||
//in the view
|
||||
demo.view = function(ctrl) {
|
||||
//This view gets rendered before the request above completes
|
||||
//Calling .map doesn't throw an error because we defined the initial value to be an empty array, instead of undefined
|
||||
return ctrl.users().map(function(user) {
|
||||
return m("div", user.name)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
[How to read signatures](how-to-read-signatures.md)
|
||||
|
||||
```clike
|
||||
Promise request(Options options)
|
||||
|
||||
where:
|
||||
Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)) }
|
||||
GetterSetter :: any getterSetter([any value])
|
||||
Options :: XHROptions | JSONPOptions
|
||||
XHROptions :: Object {
|
||||
String method,
|
||||
String url,
|
||||
[String user,]
|
||||
[String password,]
|
||||
[Object<any> data,]
|
||||
[Boolean background,]
|
||||
[any initialValue,]
|
||||
[any unwrapSuccess(any data, XMLHttpRequest xhr),]
|
||||
[any unwrapError(any data, XMLHttpRequest xhr),]
|
||||
[String serialize(any dataToSerialize),]
|
||||
[any deserialize(String dataToDeserialize),]
|
||||
[any extract(XMLHttpRequest xhr, XHROptions options),]
|
||||
[void type(Object<any> data),]
|
||||
[XMLHttpRequest? config(XMLHttpRequest xhr, XHROptions options)]
|
||||
}
|
||||
JSONPOptions :: Object {
|
||||
String dataType,
|
||||
String url,
|
||||
String callbackKey,
|
||||
String callbackName,
|
||||
Object<any> data
|
||||
}
|
||||
```
|
||||
|
||||
- **XHROptions options**
|
||||
|
||||
A map of options for the XMLHttpRequest
|
||||
|
||||
- **String method**
|
||||
|
||||
The HTTP method. Must be either `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, `"HEAD"` or `"OPTIONS"`
|
||||
|
||||
- **String url**
|
||||
|
||||
The URL to request. If the URL is not in the same domain as the application, the target server must be configured to accept cross-domain requests from the application's domain, i.e. its responses must include the header `Access-Control-Allow-Origin: *`.
|
||||
|
||||
- **String user** (optional)
|
||||
|
||||
A user for HTTP authentication. Defaults to `undefined`
|
||||
|
||||
- **String password** (optional)
|
||||
|
||||
A password for HTTP authentication. Defaults to `undefined`
|
||||
|
||||
- **Object<any> data** (optional)
|
||||
|
||||
Data to be sent. It's automatically placed in the appropriate section of the request with the appropriate serialization based on `method`
|
||||
|
||||
- **Boolean background** (optional)
|
||||
|
||||
Determines whether the `m.request` can affect template rendering. Defaults to false.
|
||||
|
||||
If this option is set to true, then the request does NOT call [`m.startComputation` / `m.endComputation`](mithril.computation.md) internally, and therefore the completion of the request does not trigger an update of the view, even if data has been changed. This option is useful for running operations in the background (i.e. without user intervention).
|
||||
|
||||
In order to force a redraw after a background request, use [`m.redraw`](mithril.redraw.md), or `m.startComputation` / `m.endComputation`.
|
||||
|
||||
```javascript
|
||||
var demo = {}
|
||||
|
||||
demo.controller = function() {
|
||||
var users = m.request({method: "GET", url: "/api/users", background: true, initialValue: []})
|
||||
users.then(m.redraw)
|
||||
return {users: users}
|
||||
}
|
||||
|
||||
demo.view = function(ctrl) {
|
||||
//this view renders twice (once immediately, and once after the request above completes)
|
||||
return m("div", [
|
||||
ctrl.users().map(function(user) {
|
||||
return m("div", user.name)
|
||||
})
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
It's strongly recommended that you set an `initialValue` option in ALL requests if you set the `background` option to true.
|
||||
|
||||
When calling multiple background AJAX requests, it's recommended that you use [`m.sync`](mithril.sync.md) to batch redraw once at the end of all requests, as opposed to repeatedly redrawing after every request:
|
||||
|
||||
```javascript
|
||||
var demo = {}
|
||||
|
||||
demo.controller = function() {
|
||||
var users = m.request({method: "GET", url: "/api/users", background: true, initialValue: []})
|
||||
var projects = m.request({method: "GET", url: "/api/projects", background: true, initialValue: []})
|
||||
|
||||
m.sync([users, projects]).then(m.redraw)
|
||||
|
||||
return {users: users, projects: projects}
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to add null checks if your request value can be null
|
||||
|
||||
```javascript
|
||||
var demo = {}
|
||||
|
||||
demo.controller = function() {
|
||||
var user = m.request({method: "GET", url: "/api/users/1", background: true, initialValue: null})
|
||||
user.then(m.redraw)
|
||||
return {user: user}
|
||||
}
|
||||
|
||||
demo.view = function(ctrl) {
|
||||
return m("div", [
|
||||
//in the first redraw, there's no user, so ensure we don't throw an error
|
||||
ctrl.user() ? ctrl.user().name : "no user"
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
- **any initialValue** (optional)
|
||||
|
||||
The value that populates the returned getter-setter before the request completes. This is useful when using the `background` option, in order to avoid the need for null checks in views that may be attempting to access the returned getter-setter before the asynchronous request resolves.
|
||||
|
||||
It is strongly recommended that you always set this option to avoid future surprises.
|
||||
|
||||
- **any unwrapSuccess(any data, XMLHttpRequest xhr)** (optional)
|
||||
|
||||
A preprocessor function to unwrap the data from a success response in case the response contains metadata wrapping the data.
|
||||
|
||||
The default value (if this parameter is falsy) is the identity function `function(value) {return value}`
|
||||
|
||||
For example, if the response is `{data: [{name: "John"}, {name: "Mary"}]}` and the unwrap function is `function(response) {return response.data}`, then the response will be considered to be `[{name: "John"}, {name: "Mary"}]` when processing the `type` parameter
|
||||
|
||||
- **Object<any> | Array<any> data**
|
||||
|
||||
The data to unwrap
|
||||
|
||||
- **returns Object<any> | Array<any> unwrappedData**
|
||||
|
||||
The unwrapped data
|
||||
|
||||
- **any unwrapError(any data, XMLHttpRequest xhr)** (optional)
|
||||
|
||||
A preprocessor function to unwrap the data from an error response in case the response contains metadata wrapping the data.
|
||||
|
||||
The default value (if this parameter is falsy) is the identity function `function(value) {return value}`
|
||||
|
||||
- **Object<any> | Array<any> data**
|
||||
|
||||
The data to unwrap
|
||||
|
||||
- **returns Object<any> | Array<any> unwrappedData**
|
||||
|
||||
The unwrapped data
|
||||
|
||||
- **String serialize(any dataToSerialize)** (optional)
|
||||
|
||||
Method to use to serialize the request data
|
||||
|
||||
The default value (if this parameter is falsy) is `JSON.stringify`
|
||||
|
||||
- **any dataToSerialize**
|
||||
|
||||
Data to be serialized
|
||||
|
||||
- **returns String serializedData**
|
||||
|
||||
- **any deserialize(String dataToDeserialize)** (optional)
|
||||
|
||||
Method to use to deserialize the response data
|
||||
|
||||
The default value (if this parameter is falsy) is `JSON.parse`
|
||||
|
||||
- **String dataToDeserialize**
|
||||
|
||||
Data to be deserialized
|
||||
|
||||
- **returns any deserializedData**
|
||||
|
||||
- **any extract(XMLHttpRequest xhr, XHROptions options)** (optional)
|
||||
|
||||
Method to use to extract the data from the raw XMLHttpRequest. This is useful when the relevant data is either in a response header or the status field.
|
||||
|
||||
If this parameter is falsy, the default value is a function that returns `xhr.responseText`.
|
||||
|
||||
- **void type(Object<any> data)** (optional)
|
||||
|
||||
The response object (or the child items if this object is an Array) will be passed as a parameter to the class constructor defined by `type`
|
||||
|
||||
If this parameter is falsy, the deserialized data will not be wrapped.
|
||||
|
||||
For example, if `type` is the following class:
|
||||
|
||||
```javascript
|
||||
var User = function(data) {
|
||||
this.name = m.prop(data.name);
|
||||
}
|
||||
```
|
||||
|
||||
And the data is `[{name: "John"}, {name: "Mary"}]`, then the response will contain an array of two User instances.
|
||||
|
||||
- **Object<String> headers** (optional)
|
||||
|
||||
Additional headers to set on the request, if any. Each header is specified as a key-value pair.
|
||||
|
||||
- **XMLHttpRequest? config(XMLHttpRequest xhr, XHROptions options)** (optional)
|
||||
|
||||
An initialization function that runs after `open` and before `send`. Useful for using XHR2 features, such as the XMLHttpRequest's `upload` property or `progress` event.
|
||||
|
||||
- **XMLHttpRequest xhr**
|
||||
|
||||
The XMLHttpRequest instance.
|
||||
|
||||
- **XHROptions options**
|
||||
|
||||
The `options` parameter that was passed into `m.request` call
|
||||
|
||||
- **returns XMLHttpRequest? xhr**
|
||||
|
||||
You may return an XHR-like object (e.g. a XDomainRequest instance) to override the provided XHR instance altogether.
|
||||
|
||||
- **returns Promise promise**
|
||||
|
||||
returns a promise that can bind callbacks which get called on completion of the AJAX request.
|
||||
|
||||
---
|
||||
|
||||
- **JSONPOptions options**
|
||||
|
||||
A map of options for JSONP requests
|
||||
|
||||
- **String dataType**
|
||||
|
||||
Must be the string "jsonp"
|
||||
|
||||
- **String url**
|
||||
|
||||
The URL to request. If the URL is not in the same domain as the application, the target server must be configured to accept cross-domain requests from the application's domain, i.e. its responses must include the header `Access-Control-Allow-Origin: *`.
|
||||
|
||||
- **String callbackKey**
|
||||
|
||||
The name of the querystring key that defines the name of the callback function to be called by the response. Defaults to "callback"
|
||||
|
||||
This option is useful for web services that use uncommon conventions for defining jsonp callbacks (e.g. foo.com/?jsonpCallback=doSomething)
|
||||
|
||||
- **String callbackName**
|
||||
|
||||
The name of callback function to be called by the response. Defaults to a unique auto-generated name
|
||||
|
||||
This option is useful for web services serving static files and to prevent cache busting.
|
||||
|
||||
- **Object<any> data** (optional)
|
||||
|
||||
Data to be sent. It's automatically placed in the appropriate section of the request with the appropriate serialization based on `method`
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
1178
index.d.ts
vendored
1178
index.d.ts
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* @fileoverview Closure Compiler externs for Mithril.
|
||||
* @see http://mithril.js.org
|
||||
* @externs
|
||||
*/
|
||||
var m = {
|
||||
"render": function () {},
|
||||
"mount": function () {},
|
||||
"trust": function () {},
|
||||
"module": function () {},
|
||||
"redraw": function () {},
|
||||
"startComputation": function () {},
|
||||
"endComputation": function () {},
|
||||
"withAttr": function () {},
|
||||
"route": function () {},
|
||||
"prop": function () {},
|
||||
"deferred": function () {},
|
||||
"sync": function () {},
|
||||
"request": function () {},
|
||||
"deps": function () {},
|
||||
"component": function() {}
|
||||
};
|
||||
871
mithril.d.ts
vendored
871
mithril.d.ts
vendored
|
|
@ -1,871 +0,0 @@
|
|||
// Mithril type definitions for Typescript. Note that changes to this *must* be
|
||||
// mirrored in the module export file in index.d.ts
|
||||
|
||||
/**
|
||||
* This is the module containing all the types/declarations/etc. for Mithril
|
||||
*/
|
||||
declare namespace Mithril {
|
||||
interface ChildArray extends Array<Children> {}
|
||||
type Children = Child | ChildArray;
|
||||
type Child = string | TrustedString | VirtualElement | Component<Controller>;
|
||||
|
||||
interface Static {
|
||||
/**
|
||||
* Creates a virtual element for use with m.render, m.mount, etc.
|
||||
*
|
||||
* @param selector A simple CSS selector. May include SVG tags. Nested
|
||||
* selectors are not supported.
|
||||
* @param attributes Attributes to add. Any DOM attribute may be used
|
||||
* as an attribute, although innerHTML and the like may be overwritten
|
||||
* silently.
|
||||
* @param children Child elements, components, and text to add.
|
||||
* @return A virtual element.
|
||||
*
|
||||
* @see m.render
|
||||
* @see m.mount
|
||||
* @see m.component
|
||||
*/
|
||||
(
|
||||
selector: string,
|
||||
...children: Children[]
|
||||
): VirtualElement;
|
||||
|
||||
/**
|
||||
* Creates a virtual element for use with m.render, m.mount, etc.
|
||||
*
|
||||
* @param selector A simple CSS selector. May include SVG tags. Nested
|
||||
* selectors are not supported.
|
||||
* @param attributes Attributes to add. Any DOM attribute may be used
|
||||
* as an attribute, although innerHTML and the like may be overwritten
|
||||
* silently.
|
||||
* @param children Child elements, components, and text to add.
|
||||
* @return A virtual element.
|
||||
*
|
||||
* @see m.render
|
||||
* @see m.mount
|
||||
* @see m.component
|
||||
*/
|
||||
(
|
||||
selector: string,
|
||||
attributes: Attributes,
|
||||
...children: Children[]
|
||||
): VirtualElement;
|
||||
|
||||
/**
|
||||
* Initializes a component for use with m.render, m.mount, etc.
|
||||
*
|
||||
* @param component A component.
|
||||
* @param args Arguments to optionally pass to the component.
|
||||
* @return A component.
|
||||
*
|
||||
* @see m.render
|
||||
* @see m.mount
|
||||
* @see m
|
||||
*/
|
||||
<T extends Controller>(
|
||||
component: Component<T>,
|
||||
...args: any[]
|
||||
): Component<T>;
|
||||
|
||||
/**
|
||||
* Creates a getter-setter function that wraps a Mithril promise. Useful
|
||||
* for uniform data access, m.withAttr, etc.
|
||||
*
|
||||
* @param promise A thennable to initialize the property with. It may
|
||||
* optionally be a Mithril promise.
|
||||
* @return A getter-setter function wrapping the promise.
|
||||
*
|
||||
* @see m.withAttr
|
||||
*/
|
||||
prop<T>(promise: Thennable<T>) : Promise<T>;
|
||||
|
||||
/**
|
||||
* Creates a getter-setter function that wraps a simple value. Useful
|
||||
* for uniform data access, m.withAttr, etc.
|
||||
*
|
||||
* @param value A value to initialize the property with
|
||||
* @return A getter-setter function wrapping the value.
|
||||
*
|
||||
* @see m.withAttr
|
||||
*/
|
||||
prop<T>(value: T): BasicProperty<T>;
|
||||
|
||||
/**
|
||||
* Creates a getter-setter function that wraps a simple value. Useful
|
||||
* for uniform data access, m.withAttr, etc.
|
||||
*
|
||||
* @return A getter-setter function wrapping the value.
|
||||
*
|
||||
* @see m.withAttr
|
||||
*/
|
||||
prop<T>(): BasicProperty<T>;
|
||||
|
||||
/**
|
||||
* Returns a event handler that can be bound to an element, firing with
|
||||
* the specified property.
|
||||
*
|
||||
* @param property The property to get from the event.
|
||||
* @param callback The handler to use the value from the event.
|
||||
* @return A function suitable for listening to an event.
|
||||
*/
|
||||
withAttr(
|
||||
property: string,
|
||||
callback: (value: any) => any,
|
||||
callbackThis?: any
|
||||
): (e: Event) => void;
|
||||
|
||||
/**
|
||||
* @deprecated Use m.mount instead
|
||||
*/
|
||||
module<T extends Controller>(
|
||||
rootElement: Node,
|
||||
component: Component<T>
|
||||
): T;
|
||||
|
||||
/**
|
||||
* Mounts a component to a base DOM node.
|
||||
*
|
||||
* @param rootElement The base node.
|
||||
* @param component The component to mount.
|
||||
* @return An instance of the top-level component's controller
|
||||
*/
|
||||
mount<T extends Controller>(
|
||||
rootElement: Node,
|
||||
component: Component<T>
|
||||
): T;
|
||||
|
||||
/**
|
||||
* Initializes a component for use with m.render, m.mount, etc.
|
||||
*
|
||||
* @param selector A component.
|
||||
* @param args Arguments to optionally pass to the component.
|
||||
* @return A component.
|
||||
*
|
||||
* @see m.render
|
||||
* @see m.mount
|
||||
* @see m
|
||||
*/
|
||||
component<T extends Controller>(
|
||||
component: Component<T>,
|
||||
...args: any[]
|
||||
): Component<T>;
|
||||
|
||||
/**
|
||||
* Trust this string of HTML.
|
||||
*
|
||||
* @param html The HTML to trust
|
||||
* @return A String object instance with an added internal flag to mark
|
||||
* it as trusted.
|
||||
*/
|
||||
trust(html: string): TrustedString;
|
||||
|
||||
/**
|
||||
* Render a virtual DOM tree.
|
||||
*
|
||||
* @param rootElement The base element/node to render the tree from.
|
||||
* @param children One or more child nodes to add to the tree.
|
||||
* @param forceRecreation If true, overwrite the entire tree without
|
||||
* diffing against it.
|
||||
*/
|
||||
render(
|
||||
rootElement: Element,
|
||||
children: VirtualElement|VirtualElement[],
|
||||
forceRecreation?: boolean
|
||||
): void;
|
||||
|
||||
redraw: {
|
||||
/**
|
||||
* Force a redraw the active component. It redraws asynchronously by
|
||||
* default to allow for simultaneous events to run before redrawing,
|
||||
* such as the event combination keypress + input frequently used for
|
||||
* input.
|
||||
*
|
||||
* @param force If true, redraw synchronously.
|
||||
*/
|
||||
(force?: boolean): void;
|
||||
|
||||
/**
|
||||
* Gets/sets the current redraw strategy, which returns one of the
|
||||
* following:
|
||||
*
|
||||
* "all" - recreates the DOM tree from scratch
|
||||
* "diff" - recreates the DOM tree from scratch
|
||||
* "none" - leaves the DOM tree intact
|
||||
*
|
||||
* This is useful for event handlers, which may want to cancel
|
||||
* the next redraw if the event doesn't update the UI.
|
||||
*
|
||||
* @return The current strategy
|
||||
*/
|
||||
strategy: BasicProperty<"all" | "diff" | "none">;
|
||||
}
|
||||
|
||||
route: {
|
||||
/**
|
||||
* Enable routing, mounting a controller based on the route. It
|
||||
* automatically mounts the components for you, starting with the one
|
||||
* specified by the default route.
|
||||
*
|
||||
* @param rootElement The element to mount the active controller to.
|
||||
* @param defaultRoute The route to start with.
|
||||
* @param routes A key-value mapping of pathname to controller.
|
||||
*/
|
||||
(
|
||||
rootElement: Element,
|
||||
defaultRoute: string,
|
||||
routes: Routes
|
||||
): void;
|
||||
|
||||
/**
|
||||
* This allows m.route to be used as the `config` attribute for a
|
||||
* virtual element, particularly useful for cases like this:
|
||||
*
|
||||
* ```ts
|
||||
* // Note that the '#' is not required in `href`, thanks to the
|
||||
* `config` setting.
|
||||
* m("a[href='/dashboard/alicesmith']", {config: m.route});
|
||||
* ```
|
||||
*/
|
||||
(
|
||||
element: Element,
|
||||
isInitialized: boolean,
|
||||
context?: Context,
|
||||
vdom?: VirtualElement
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Programmatically redirect to another route.
|
||||
*
|
||||
* @param path The route to go to.
|
||||
* @param params Parameters to pass as a query string.
|
||||
* @param shouldReplaceHistory Whether to replace the current history
|
||||
* instead of adding a new one.
|
||||
*/
|
||||
(path: string, params?: any, shouldReplaceHistory?: boolean): void;
|
||||
|
||||
/**
|
||||
* Gets the current route.
|
||||
*
|
||||
* @return The current route.
|
||||
*/
|
||||
(): string;
|
||||
|
||||
/**
|
||||
* Gets a route parameter.
|
||||
*
|
||||
* @param key The key to get.
|
||||
* @return The value associated with the parameter key.
|
||||
*/
|
||||
param(key: string): string;
|
||||
|
||||
/**
|
||||
* Gets all route parameters.
|
||||
*
|
||||
* @return All route parameters.
|
||||
*/
|
||||
param(): Object;
|
||||
|
||||
/**
|
||||
* The current routing mode. This may be changed before calling
|
||||
* m.route to change the part of the URL used to perform the routing.
|
||||
*
|
||||
* The value can be set to one of the following, defaulting to
|
||||
* "hash":
|
||||
*
|
||||
* "search" - Uses the query string. This allows for named anchors to
|
||||
* work on the page, but changes cause IE8 and lower to refresh the
|
||||
* page.
|
||||
*
|
||||
* "hash" - Uses the hash. This is the only routing mode that does
|
||||
* not cause page refreshes on any browser, but it does not support
|
||||
* named anchors.
|
||||
*
|
||||
* "pathname" - Uses the URL pathname. This requires server-side
|
||||
* setup to support bookmarking and page refreshes. It always causes
|
||||
* page refreshes on IE8 and lower. Note that this requires that the
|
||||
* application to be run from the root of the URL.
|
||||
*/
|
||||
mode: "search" | "hash" | "pathname";
|
||||
|
||||
/**
|
||||
* Serialize an object into a query string.
|
||||
*
|
||||
* @param data The data to serialize.
|
||||
* @return The serialized string.
|
||||
*/
|
||||
buildQueryString(data: Object): string;
|
||||
|
||||
/**
|
||||
* Parse a query string into an object.
|
||||
*
|
||||
* @param data The data to parse.
|
||||
* @return The parsed object data.
|
||||
*/
|
||||
parseQueryString(data: string): Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an XHR request to a server. Note that the `url` option is
|
||||
* required.
|
||||
*
|
||||
* @param options The options to use for the request.
|
||||
* @return A promise to the returned data, or void if not applicable.
|
||||
*
|
||||
* @see XHROptions for the available options.
|
||||
*/
|
||||
request(options: XHROptions): Promise<any>
|
||||
|
||||
/**
|
||||
* Send a JSONP request to a server. Note that the `url` option is
|
||||
* required.
|
||||
*
|
||||
* @param options The options to use
|
||||
* @return A promise to the returned data.
|
||||
*
|
||||
* @see JSONPOptions for the available options.
|
||||
*/
|
||||
request(options: JSONPOptions): Promise<any>;
|
||||
|
||||
deferred: {
|
||||
/**
|
||||
* Create a Mithril deferred object. It behaves synchronously if
|
||||
* possible, an intentional deviation from Promises/A+. Note that
|
||||
* deferreds are completely separate from the redrawing system, and
|
||||
* never trigger a redraw on their own.
|
||||
*
|
||||
* @return A new Mithril deferred instance.
|
||||
*
|
||||
* @see m.deferred.onerror for the error callback called for Error
|
||||
* subclasses
|
||||
*/
|
||||
<T>(): Deferred<T>;
|
||||
|
||||
/**
|
||||
* A callback for all uncaught native Error subclasses in deferreds.
|
||||
* This defaults to synchronously rethrowing all errors, a deviation
|
||||
* from Promises/A+, but the behavior is configurable. To restore
|
||||
* Promises/A+-compatible behavior. simply set this to a no-op.
|
||||
*/
|
||||
onerror(e: Error): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of promises or thennables and returns a Mithril promise
|
||||
* that resolves once all in the list are resolved, or rejects if any of
|
||||
* them reject.
|
||||
*
|
||||
* @param promises A list of promises to try to resolve.
|
||||
* @return A promise that resolves to all the promises if all resolve, or
|
||||
* rejects with the error contained in the first rejection.
|
||||
*/
|
||||
sync<T>(promises: Thennable<T>[]): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* Use this and endComputation if your views aren't redrawing after
|
||||
* calls to third-party libraries. For integrating asynchronous code,
|
||||
* this should be called before any asynchronous work is done. For
|
||||
* synchronous code, this should be called at the beginning of the
|
||||
* problematic segment. Note that these calls must be balanced, much like
|
||||
* braces and parentheses. This is mostly used internally. Prefer
|
||||
* m.redraw where possible, especially when making repeated calls.
|
||||
*
|
||||
* @see endComputation
|
||||
* @see m.render
|
||||
*/
|
||||
startComputation(): void;
|
||||
|
||||
/**
|
||||
* Use startComputation and this if your views aren't redrawing after
|
||||
* calls to third-party libraries. For integrating asynchronous code,
|
||||
* this should be called after all asynchronous work completes. For
|
||||
* synchronous code, this should be called at the end of the problematic
|
||||
* segment. Note that these calls must be balanced, much like braces and
|
||||
* parentheses. This is mostly used internally. Prefer m.redraw where
|
||||
* possible, especially when making repeated calls.
|
||||
*
|
||||
* @see startComputation
|
||||
* @see m.render
|
||||
*/
|
||||
endComputation(): void;
|
||||
|
||||
/**
|
||||
* This overwrites the internal version of window used by Mithril.
|
||||
* It's mostly useful for testing, and is also used internally by
|
||||
* Mithril to test itself. By default Mithril uses `window` for the
|
||||
* dependency.
|
||||
*
|
||||
* @param mockWindow The mock to use for the window.
|
||||
* @return The mock that was passed in.
|
||||
*/
|
||||
deps(mockWindow: Window): Window;
|
||||
}
|
||||
|
||||
interface TrustedString extends String {
|
||||
/** @private Implementation detail. Don't depend on it. */
|
||||
$trusted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a virtual element. It's best to consider this immutable
|
||||
* for most use cases.
|
||||
*
|
||||
* @see m
|
||||
*/
|
||||
interface VirtualElement {
|
||||
/**
|
||||
* The tag name of this element.
|
||||
*/
|
||||
tag: string;
|
||||
|
||||
/**
|
||||
* The attributes of this element.
|
||||
*/
|
||||
attrs: Attributes;
|
||||
|
||||
/**
|
||||
* The children of this element.
|
||||
*/
|
||||
children: Children[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An event passed by Mithril to unload event handlers.
|
||||
*/
|
||||
interface Event {
|
||||
/**
|
||||
* Prevent the default behavior of scrolling the page and updating the
|
||||
* URL on next route change.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A context object for configuration functions.
|
||||
*
|
||||
* @see ElementConfig
|
||||
*/
|
||||
interface Context {
|
||||
/**
|
||||
* A function to call when the node is unloaded. Useful for cleanup.
|
||||
*/
|
||||
onunload?(): any;
|
||||
|
||||
/**
|
||||
* Set true if the backing DOM node needs to be retained between route
|
||||
* changes if possible. Set false if this node needs to be recreated
|
||||
* every single time, regardless of how "different" it is.
|
||||
*/
|
||||
retain?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a callback function for a virtual element's config
|
||||
* attribute. It's a low-level function useful for extra cleanup after
|
||||
* removal from the tree, storing instances of third-party classes that
|
||||
* need to be associated with the DOM, etc.
|
||||
*
|
||||
* @see Attributes
|
||||
* @see Context
|
||||
*/
|
||||
interface ElementConfig {
|
||||
/**
|
||||
* A callback function for a virtual element's config attribute.
|
||||
*
|
||||
* @param element The associated DOM element.
|
||||
* @param isInitialized Whether this is the first call for the virtual
|
||||
* element or not.
|
||||
* @param context The associated context for this element.
|
||||
* @param vdom The associated virtual element.
|
||||
*/
|
||||
(
|
||||
element: Element,
|
||||
isInitialized: boolean,
|
||||
context: Context,
|
||||
vdom: VirtualElement
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents the attributes available for configuring virtual elements,
|
||||
* beyond the applicable DOM attributes.
|
||||
*
|
||||
* @see m
|
||||
*/
|
||||
interface Attributes {
|
||||
/**
|
||||
* The class name(s) for this virtual element, as a space-separated list.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The class name(s) for this virtual element, as a space-separated list.
|
||||
*/
|
||||
class?: string;
|
||||
|
||||
/**
|
||||
* A custom, low-level configuration in case this element needs special
|
||||
* cleanup after removal from the tree.
|
||||
*
|
||||
* @see ElementConfig
|
||||
*/
|
||||
config?: ElementConfig;
|
||||
|
||||
/**
|
||||
* A key to optionally associate with this element.
|
||||
*/
|
||||
key?: string | number;
|
||||
|
||||
/**
|
||||
* Any other virtual element properties, including attributes and event
|
||||
* handlers.
|
||||
*/
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The basis of a Mithril controller instance.
|
||||
*/
|
||||
interface Controller {
|
||||
/**
|
||||
* An optional handler to call when the associated virtual element is
|
||||
* destroyed.
|
||||
*
|
||||
* @param evt An associated event.
|
||||
*/
|
||||
onunload?(evt: Event): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a controller function.
|
||||
*
|
||||
* @see ControllerConstructor
|
||||
*/
|
||||
interface ControllerFunction<T extends Controller> {
|
||||
(...args: any[]): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a controller constructor.
|
||||
*
|
||||
* @see ControllerFunction
|
||||
*/
|
||||
interface ControllerConstructor<T extends Controller> {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a Mithril component.
|
||||
*
|
||||
* @see m
|
||||
* @see m.component
|
||||
*/
|
||||
interface Component<T extends Controller> {
|
||||
/**
|
||||
* The component's controller.
|
||||
*
|
||||
* @see m.component
|
||||
*/
|
||||
controller?: ControllerFunction<T> | ControllerConstructor<T>;
|
||||
|
||||
/**
|
||||
* Creates a view out of virtual elements.
|
||||
*
|
||||
* @see m.component
|
||||
*/
|
||||
view(ctrl?: T, ...args: any[]): VirtualElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the base interface for property getter-setters
|
||||
*
|
||||
* @see m.prop
|
||||
*/
|
||||
interface Property<T> {
|
||||
/**
|
||||
* Gets the contained value.
|
||||
*
|
||||
* @return The contained value.
|
||||
*/
|
||||
(): T;
|
||||
|
||||
/**
|
||||
* Sets the contained value.
|
||||
*
|
||||
* @param value The new value to set.
|
||||
* @return The newly set value.
|
||||
*/
|
||||
(value: T): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a non-promise getter-setter functions.
|
||||
*
|
||||
* @see m.prop which returns objects that implement this interface.
|
||||
*/
|
||||
interface BasicProperty<T> extends Property<T> {
|
||||
/**
|
||||
* Makes this serializable to JSON.
|
||||
*/
|
||||
toJSON(): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a key-value mapping linking routes to components.
|
||||
*/
|
||||
interface Routes {
|
||||
/**
|
||||
* The key represents the route. The value represents the corresponding
|
||||
* component.
|
||||
*/
|
||||
[key: string]: Component<Controller>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a Mithril deferred object.
|
||||
*/
|
||||
interface Deferred<T> {
|
||||
/**
|
||||
* Resolve this deferred's promise with a value.
|
||||
*
|
||||
* @param value The value to resolve the promise with.
|
||||
*/
|
||||
resolve(value?: T): void;
|
||||
|
||||
/**
|
||||
* Reject this deferred with an error.
|
||||
*
|
||||
* @param value The reason for rejecting the promise.
|
||||
*/
|
||||
reject(reason?: any): void;
|
||||
|
||||
/**
|
||||
* The backing promise.
|
||||
*
|
||||
* @see Promise
|
||||
*/
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a thennable success callback.
|
||||
*/
|
||||
interface SuccessCallback<T, U> {
|
||||
(value: T): U | Thennable<U>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a thennable error callback.
|
||||
*/
|
||||
interface ErrorCallback<T> {
|
||||
(value: Error): T | Thennable<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a thennable.
|
||||
*/
|
||||
interface Thennable<T> {
|
||||
then<U>(success: SuccessCallback<T, U>): Thennable<U>;
|
||||
then<U, V>(success: SuccessCallback<T, U>, error: ErrorCallback<V>): Thennable<U | V>;
|
||||
catch?(error: ErrorCallback<T>): Thennable<T>;
|
||||
catch?<U>(error: ErrorCallback<U>): Thennable<T | U>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a Mithril promise object.
|
||||
*/
|
||||
interface Promise<T> extends Thennable<T>, Property<T | Promise<T>> {
|
||||
/**
|
||||
* Chain this promise with a simple success callback, propogating
|
||||
* rejections.
|
||||
*
|
||||
* @param success The callback to call when the promise is resolved.
|
||||
* @return The chained promise.
|
||||
*/
|
||||
then<U>(success: SuccessCallback<T, U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* Chain this promise with a success callback and error callback, without
|
||||
* propogating rejections.
|
||||
*
|
||||
* @param success The callback to call when the promise is resolved.
|
||||
* @param error The callback to call when the promise is rejected.
|
||||
* @return The chained promise.
|
||||
*/
|
||||
then<U, V>(success: SuccessCallback<T, U>, error: ErrorCallback<V>): Promise<U | V>;
|
||||
|
||||
/**
|
||||
* Chain this promise with a single error callback, without propogating
|
||||
* rejections.
|
||||
*
|
||||
* @param error The callback to call when the promise is rejected.
|
||||
* @return The chained promise.
|
||||
*/
|
||||
catch<U>(error: ErrorCallback<U>): Promise<T | U>;
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the common options shared across normal and JSONP requests.
|
||||
*
|
||||
* @see m.request
|
||||
*/
|
||||
interface RequestOptions {
|
||||
/**
|
||||
* The data to be sent. It's automatically serialized in the right format
|
||||
* depending on the method (with exception of HTML5 FormData), and put in
|
||||
* the appropriate section of the request.
|
||||
*/
|
||||
data?: any;
|
||||
|
||||
/**
|
||||
* Whether to run it in the background, i.e. true if it doesn't affect
|
||||
* template rendering.
|
||||
*/
|
||||
background?: boolean;
|
||||
|
||||
/**
|
||||
* Set an initial value while the request is working, to populate the
|
||||
* promise getter-setter.
|
||||
*/
|
||||
initialValue?: any;
|
||||
|
||||
/**
|
||||
* An optional preprocessor function to unwrap a successful response, in
|
||||
* case the response contains metadata wrapping the data.
|
||||
*
|
||||
* @param data The data to unwrap.
|
||||
* @return The unwrapped result.
|
||||
*/
|
||||
unwrapSuccess?(data: any): any;
|
||||
|
||||
/**
|
||||
* An optional preprocessor function to unwrap an unsuccessful response,
|
||||
* in case the response contains metadata wrapping the data.
|
||||
*
|
||||
* @param data The data to unwrap.
|
||||
* @return The unwrapped result.
|
||||
*/
|
||||
unwrapError?(data: any): any;
|
||||
|
||||
/**
|
||||
* An optional function to serialize the data. This defaults to
|
||||
* `JSON.stringify`.
|
||||
*
|
||||
* @param dataToSerialize The data to serialize.
|
||||
* @return The serialized form as a string.
|
||||
*/
|
||||
serialize?(dataToSerialize: any): string;
|
||||
|
||||
/**
|
||||
* An optional function to deserialize the data. This defaults to
|
||||
* `JSON.parse`.
|
||||
*
|
||||
* @param dataToSerialize The data to parse.
|
||||
* @return The parsed form.
|
||||
*/
|
||||
deserialize?(dataToDeserialize: string): any;
|
||||
|
||||
/**
|
||||
* An optional function to extract the data from a raw XMLHttpRequest,
|
||||
* useful if the relevant data is in a response header or the status
|
||||
* field.
|
||||
*
|
||||
* @param xhr The associated XMLHttpRequest.
|
||||
* @param options The options passed to this request.
|
||||
* @return string The serialized format.
|
||||
*/
|
||||
extract?(xhr: XMLHttpRequest, options: this): string;
|
||||
|
||||
/**
|
||||
* The parsed data, or its children if it's an array, will be passed to
|
||||
* this class constructor if it's given, to parse it into classes.
|
||||
*
|
||||
* @param data The data to parse.
|
||||
* @return The new instance for the list.
|
||||
*/
|
||||
type?: new (data: any) => any;
|
||||
|
||||
/**
|
||||
* The URL to send the request to.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents the available options for configuring m.request for JSONP
|
||||
* requests.
|
||||
*
|
||||
* @see m.request
|
||||
*/
|
||||
interface JSONPOptions extends RequestOptions {
|
||||
/**
|
||||
* For JSONP requests, this must be the string "jsonp". Otherwise, it's
|
||||
* ignored.
|
||||
*/
|
||||
dataType: "jsonp";
|
||||
|
||||
/**
|
||||
* The querystring key for the JSONP request callback. This is useful for
|
||||
* APIs that don't use common conventions, such as
|
||||
* `www.example.com/?jsonpCallback=doSomething`. It defaults to
|
||||
* `callback`.
|
||||
*/
|
||||
callbackKey?: string;
|
||||
|
||||
/**
|
||||
* The data to send with the request. This is automatically serialized
|
||||
* to a querystring.
|
||||
*/
|
||||
data?: Object;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents the available options for configuring m.request for
|
||||
* standard AJAX requests.
|
||||
*
|
||||
* @see m.request
|
||||
*/
|
||||
interface XHROptions extends RequestOptions {
|
||||
/**
|
||||
* This represents the HTTP method used, defaulting to "GET".
|
||||
*/
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
|
||||
|
||||
/**
|
||||
* The username for HTTP authentication.
|
||||
*/
|
||||
user?: string;
|
||||
|
||||
/**
|
||||
* The password for HTTP authentication.
|
||||
*/
|
||||
password?: string;
|
||||
|
||||
/**
|
||||
* An optional object map of headers with their respective values
|
||||
*/
|
||||
headers?: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* An optional function to run between `open` and `send`, useful for
|
||||
* adding request headers or using XHR2 features such as the `upload`
|
||||
* property. It is even possible to override the XHR altogether with a
|
||||
* similar object, such as an XDomainRequest instance.
|
||||
*
|
||||
* @param xhr The associated XMLHttpRequest.
|
||||
* @param options The options passed to this request.
|
||||
* @return The new XMLHttpRequest, or nothing if the same one is kept.
|
||||
*/
|
||||
config?(xhr: XMLHttpRequest, options: this): any;
|
||||
|
||||
/**
|
||||
* The data to send with the request.
|
||||
*/
|
||||
data?: Object;
|
||||
}
|
||||
}
|
||||
|
||||
declare const m: Mithril.Static;
|
||||
|
||||
declare module "mithril" {
|
||||
export = m;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
71
test.html
71
test.html
|
|
@ -1,71 +0,0 @@
|
|||
<!doctype html>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.9/es5-shim.js"></script>
|
||||
<script src="mithril.js"></script>
|
||||
<body>
|
||||
<script>
|
||||
//this application only has one component: todo
|
||||
var todo = {};
|
||||
|
||||
//for simplicity, we use this component to namespace the model classes
|
||||
|
||||
//the Todo class has two properties
|
||||
todo.Todo = function(data) {
|
||||
this.description = m.prop(data.description);
|
||||
this.done = m.prop(false);
|
||||
};
|
||||
|
||||
//the TodoList class is a list of Todo's
|
||||
todo.TodoList = Array;
|
||||
|
||||
//the view-model tracks a running list of todos,
|
||||
//stores a description for new todos before they are created
|
||||
//and takes care of the logic surrounding when adding is permitted
|
||||
//and clearing the input after adding a todo to the list
|
||||
todo.vm = (function() {
|
||||
var vm = {}
|
||||
vm.init = function() {
|
||||
//a running list of todos
|
||||
vm.list = new todo.TodoList();
|
||||
|
||||
//a slot to store the name of a new todo before it is created
|
||||
vm.description = m.prop("");
|
||||
|
||||
//adds a todo to the list, and clears the description field for user convenience
|
||||
vm.add = function() {
|
||||
if (vm.description()) {
|
||||
vm.list.push(new todo.Todo({description: vm.description()}));
|
||||
vm.description("");
|
||||
}
|
||||
};
|
||||
}
|
||||
return vm
|
||||
}())
|
||||
|
||||
//the controller defines what part of the model is relevant for the current page
|
||||
//in our case, there's only one view-model that handles everything
|
||||
todo.controller = function() {
|
||||
todo.vm.init()
|
||||
}
|
||||
|
||||
//here's the view
|
||||
todo.view = function() {
|
||||
return [
|
||||
m("input", {onchange: m.withAttr("value", todo.vm.description), value: todo.vm.description()}),
|
||||
m("button", {onclick: todo.vm.add}, "Add"),
|
||||
m("table", [
|
||||
todo.vm.list.map(function(task, index) {
|
||||
return m("tr", [
|
||||
m("td", [
|
||||
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
|
||||
]),
|
||||
m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}}, task.description()),
|
||||
])
|
||||
})
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
//initialize the application
|
||||
m.mount(document.body, {controller: todo.controller, view: todo.view});
|
||||
</script>
|
||||
</body>
|
||||
228
test/mithril.js
228
test/mithril.js
|
|
@ -1,228 +0,0 @@
|
|||
describe("m.version()", function () {
|
||||
"use strict"
|
||||
|
||||
it("exists", function () {
|
||||
expect(m.version).to.be.a("function")
|
||||
})
|
||||
|
||||
it("is a string", function () {
|
||||
expect(m.version()).to.be.a("string")
|
||||
})
|
||||
})
|
||||
|
||||
describe("m()", function () {
|
||||
"use strict"
|
||||
|
||||
it("exists", function () {
|
||||
expect(m).to.be.a("function")
|
||||
})
|
||||
|
||||
it("sets correct tag name", function () {
|
||||
expect(m("div")).to.have.property("tag", "div")
|
||||
})
|
||||
|
||||
it("sets correct tag name with only a class", function () {
|
||||
expect(m(".foo")).to.have.property("tag", "div")
|
||||
})
|
||||
|
||||
it("sets correct class name", function () {
|
||||
expect(m(".foo")).to.have.deep.property("attrs.className", "foo")
|
||||
})
|
||||
|
||||
it("sets correct tag name with only an attr", function () {
|
||||
expect(m("[title=bar]")).to.have.property("tag", "div")
|
||||
})
|
||||
|
||||
it("sets correct unquoted attr", function () {
|
||||
expect(m("[title=bar]")).to.have.deep.property("attrs.title", "bar")
|
||||
})
|
||||
|
||||
it("sets attr without a value as an empty string", function () {
|
||||
expect(m("[empty]")).to.have.deep.property("attrs.empty", true)
|
||||
})
|
||||
|
||||
it("sets correct single quoted attr", function () {
|
||||
expect(m("[title=\'bar\']")).to.have.deep.property("attrs.title", "bar")
|
||||
})
|
||||
|
||||
it("sets correct double quoted attr", function () {
|
||||
expect(m("[title=\"bar\"]")).to.have.deep.property("attrs.title", "bar")
|
||||
})
|
||||
|
||||
it("sets correct children with 1 string arg", function () {
|
||||
expect(m("div", "test"))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["test"])
|
||||
})
|
||||
|
||||
it("sets correct children with multiple string args", function () {
|
||||
expect(m("div", "test", "test2"))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["test", "test2"])
|
||||
})
|
||||
|
||||
it("sets correct children with string array", function () {
|
||||
expect(m("div", ["test"]))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["test"])
|
||||
})
|
||||
|
||||
it("sets correct attrs with object", function () {
|
||||
expect(m("div", {title: "bar"}, "test"))
|
||||
.to.have.deep.property("attrs.title", "bar")
|
||||
})
|
||||
|
||||
it("sets correct children with attrs object", function () {
|
||||
expect(m("div", {title: "bar"}, "test"))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["test"])
|
||||
})
|
||||
|
||||
it("sets correct children with nested node", function () {
|
||||
expect(m("div", {title: "bar"}, m("div")))
|
||||
.to.have.property("children")
|
||||
.that.eqls([m("div")])
|
||||
})
|
||||
|
||||
it("sets correct children with string rest arg", function () {
|
||||
expect(m("div", {title: "bar"}, "test0", "test1", "test2", "test3"))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["test0", "test1", "test2", "test3"])
|
||||
})
|
||||
|
||||
it("sets correct children with node rest arg", function () {
|
||||
expect(m("div", {title: "bar"}, m("div"), m("i"), m("span")))
|
||||
.to.have.property("children")
|
||||
.that.eqls([m("div"), m("i"), m("span")])
|
||||
})
|
||||
|
||||
it("sets correct children with string array & no attrs", function () {
|
||||
expect(m("div", ["a", "b"]))
|
||||
.to.have.property("children")
|
||||
.that.eqls(["a", "b"])
|
||||
})
|
||||
|
||||
it("sets correct children with node array & no attrs", function () {
|
||||
expect(m("div", [m("div"), m("i")]))
|
||||
.to.have.property("children")
|
||||
.that.eqls([m("div"), m("i")])
|
||||
})
|
||||
|
||||
it("sets correct children with 2nd arg as node", function () {
|
||||
expect(m("div", m("div"))).to.have.property("children")
|
||||
.that.eqls([m("div")])
|
||||
})
|
||||
|
||||
it("sets correct tag with undefined array entry", function () {
|
||||
expect(m("div", [undefined])).to.have.property("tag", "div")
|
||||
})
|
||||
|
||||
it("loosely accepts invalid objects", function () {
|
||||
expect(function () { m("div", [{foo: "bar"}]) }).to.not.throw()
|
||||
})
|
||||
|
||||
it("accepts svg nodes", function () {
|
||||
expect(m("svg", [m("g")]))
|
||||
.to.have.property("children")
|
||||
.that.eqls([m("g")])
|
||||
})
|
||||
|
||||
it("accepts HTML children in svg element", function () {
|
||||
expect(m("svg", [m("a[href='http://google.com']")]))
|
||||
.to.have.property("children")
|
||||
.that.eqls([m("a[href='http://google.com']")])
|
||||
})
|
||||
|
||||
it("uses className if given", function () {
|
||||
expect(m(".foo", {className: ""}))
|
||||
.to.have.deep.property("attrs.className", "foo")
|
||||
})
|
||||
|
||||
it("accepts a class and class attr", function () {
|
||||
var node = m(".foo", {class: "bar"})
|
||||
expect(node).to.have.deep.property("attrs.class")
|
||||
expect(node.attrs.class).to.include("foo").and.include("bar")
|
||||
})
|
||||
|
||||
it("accepts a class and className attr", function () {
|
||||
var node = m(".foo", {className: "bar"})
|
||||
expect(node).to.have.deep.property("attrs.className")
|
||||
expect(node.attrs.className).to.include("foo").and.include("bar")
|
||||
})
|
||||
|
||||
// https://github.com/lhorie/mithril.js/issues/382 and 512
|
||||
it("sets an empty className attr if it's an empty string", function () {
|
||||
expect(m("div", {className: ""}))
|
||||
.to.have.deep.property("attrs.className", "")
|
||||
})
|
||||
|
||||
it("does not set className attr if class is given", function () {
|
||||
expect(m("div", {class: ""})).to.not.have.property("attrs.className")
|
||||
})
|
||||
|
||||
it("does not set class attr if className is given", function () {
|
||||
expect(m("div", {className: ""})).to.not.have.property("attrs.class")
|
||||
})
|
||||
|
||||
it("sets an empty class attr if it's an empty string", function () {
|
||||
expect(m("div", {class: ""})).to.have.deep.property("attrs.class", "")
|
||||
})
|
||||
|
||||
it("does not flatten 1 nested array", function () {
|
||||
expect(m("div", [1, 2, 3], 4))
|
||||
.to.have.property("children")
|
||||
.that.eqls([[1, 2, 3], 4])
|
||||
})
|
||||
|
||||
it("does not flatten 2 nested arrays", function () {
|
||||
expect(m("div", [1, 2, 3], [4, 5, 6, 7]))
|
||||
.to.have.property("children")
|
||||
.that.eqls([[1, 2, 3], [4, 5, 6, 7]])
|
||||
})
|
||||
|
||||
it("does not flatten 3 nested arrays", function () {
|
||||
expect(m("div", [1], [2], [3]))
|
||||
.to.have.property("children")
|
||||
.that.eqls([[1], [2], [3]])
|
||||
})
|
||||
|
||||
it("doesn't recreate the DOM when classes are different", function () {
|
||||
var v1 = m(".foo", {class: "", onclick: function () {}})
|
||||
var v2 = m(".foo", {class: "bar", onclick: function () {}})
|
||||
|
||||
expect(v1)
|
||||
.to.have.property("attrs")
|
||||
.that.contains.all.keys("class", "onclick")
|
||||
|
||||
expect(v2)
|
||||
.to.have.property("attrs")
|
||||
.that.contains.all.keys("class", "onclick")
|
||||
})
|
||||
|
||||
it("proxies an object first arg to m.component()", function () {
|
||||
var spy = sinon.spy()
|
||||
|
||||
var component = {
|
||||
controller: spy,
|
||||
view: function () {
|
||||
return m("div", "testing")
|
||||
}
|
||||
}
|
||||
|
||||
var args = {age: 12}
|
||||
|
||||
m(component, args).controller()
|
||||
expect(spy.firstCall).to.have.been.calledWith(args)
|
||||
|
||||
m.component(component, args).controller()
|
||||
expect(spy.secondCall).to.have.been.calledWith(args)
|
||||
})
|
||||
|
||||
it("does not proxy to m.component() if the object does not have .view() method", function () {
|
||||
var component = {}
|
||||
|
||||
var args = {age: 12}
|
||||
|
||||
expect(m.bind(m, component, args)).to.throw()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
describe("m.prop()", function () {
|
||||
"use strict"
|
||||
|
||||
it("reads correct value", function () {
|
||||
var prop = m.prop("test")
|
||||
expect(prop()).to.equal("test")
|
||||
})
|
||||
|
||||
it("defaults to `undefined`", function () {
|
||||
var prop = m.prop()
|
||||
expect(prop()).to.be.undefined
|
||||
})
|
||||
|
||||
it("sets the correct value", function () {
|
||||
var prop = m.prop("test")
|
||||
prop("foo")
|
||||
expect(prop()).to.equal("foo")
|
||||
})
|
||||
|
||||
it("sets `null`", function () {
|
||||
var prop = m.prop(null)
|
||||
expect(prop()).to.be.null
|
||||
})
|
||||
|
||||
it("sets `undefined`", function () {
|
||||
var prop = m.prop(undefined)
|
||||
expect(prop()).to.be.undefined
|
||||
})
|
||||
|
||||
it("returns the new value when set", function () {
|
||||
var prop = m.prop()
|
||||
expect(prop("foo")).to.equal("foo")
|
||||
})
|
||||
|
||||
it("correctly stringifies to the correct value", function () {
|
||||
var prop = m.prop("test")
|
||||
expect(JSON.stringify(prop)).to.equal('"test"')
|
||||
})
|
||||
|
||||
it("correctly stringifies to the correct value as a child", function () {
|
||||
var obj = {prop: m.prop("test")}
|
||||
expect(JSON.stringify(obj)).to.equal('{"prop":"test"}')
|
||||
})
|
||||
|
||||
it("correctly stringifies Date", function () {
|
||||
var prop = m.prop(new Date(999))
|
||||
expect(JSON.stringify(prop)).to.equal('"1970-01-01T00:00:00.999Z"')
|
||||
})
|
||||
|
||||
it("correctly stringifies object with toJSON method", function () {
|
||||
function Thing(name) {
|
||||
this.name = name
|
||||
}
|
||||
Thing.prototype.toJSON = function() {
|
||||
return {kind: 'Thing', name: this.name}
|
||||
}
|
||||
var banana = m.prop(new Thing("bannana"))
|
||||
expect(JSON.stringify(banana)).to.equal('{"kind":"Thing","name":"bannana"}')
|
||||
})
|
||||
|
||||
it("correctly wraps Mithril promises", function () {
|
||||
var defer = m.deferred()
|
||||
var prop = m.prop(defer.promise)
|
||||
defer.resolve("test")
|
||||
|
||||
expect(prop()).to.equal("test")
|
||||
})
|
||||
|
||||
it("returns a thenable when wrapping a Mithril promise", function () {
|
||||
var defer = m.deferred()
|
||||
|
||||
var prop = m.prop(defer.promise).then(function () {
|
||||
return "test2"
|
||||
})
|
||||
|
||||
defer.resolve("test")
|
||||
|
||||
expect(prop()).to.equal("test2")
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,406 +0,0 @@
|
|||
describe("m.request()", function () {
|
||||
"use strict"
|
||||
|
||||
// Much easier to read
|
||||
function resolve() {
|
||||
var xhr = mock.XMLHttpRequest.$instances.pop()
|
||||
xhr.$resolve.apply(xhr, arguments)
|
||||
xhr.onreadystatechange()
|
||||
return xhr
|
||||
}
|
||||
|
||||
// Common abstraction: request(opts, ...callbacks)
|
||||
function request(opts) {
|
||||
var ret = m.request(opts)
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
ret = ret.then(arguments[i])
|
||||
}
|
||||
resolve()
|
||||
return ret
|
||||
}
|
||||
|
||||
it("sets the correct properties on `GET`", function () {
|
||||
var prop = request({
|
||||
method: "GET",
|
||||
url: "test"
|
||||
})
|
||||
|
||||
expect(prop()).to.contain.keys({
|
||||
method: "GET",
|
||||
url: "test"
|
||||
})
|
||||
})
|
||||
|
||||
it("returns a Mithril promise (1)", function () {
|
||||
var prop = request(
|
||||
{method: "GET", url: "test"},
|
||||
function () { return "foo" })
|
||||
|
||||
expect(prop()).to.equal("foo")
|
||||
})
|
||||
|
||||
it("returns a Mithril promise (2)", function () {
|
||||
var prop = request({method: "GET", url: "test"})
|
||||
var result = prop()
|
||||
|
||||
expect(prop.then(function (value) { return value })()).to.equal(result)
|
||||
})
|
||||
|
||||
it("sets the correct properties on `POST`", function () {
|
||||
var prop = request({
|
||||
method: "POST",
|
||||
url: "http://domain.com:80",
|
||||
data: {}
|
||||
})
|
||||
|
||||
expect(prop()).to.contain.keys({
|
||||
method: "POST",
|
||||
url: "http://domain.com:80"
|
||||
})
|
||||
})
|
||||
|
||||
it("sets the correct arguments", function () {
|
||||
expect(request({
|
||||
method: "POST",
|
||||
url: "http://domain.com:80/:test1",
|
||||
data: {test1: "foo"}
|
||||
})().url).to.equal("http://domain.com:80/foo")
|
||||
})
|
||||
|
||||
it("propagates errors through the promise (1)", function () {
|
||||
var error = m.prop()
|
||||
|
||||
var prop = m.request({
|
||||
method: "GET",
|
||||
url: "test",
|
||||
deserialize: function () { throw new Error("error occurred") }
|
||||
}).then(null, error)
|
||||
resolve()
|
||||
|
||||
expect(prop().message).to.equal("error occurred")
|
||||
expect(error().message).to.equal("error occurred")
|
||||
})
|
||||
|
||||
it("propagates errors through the promise (2)", function () {
|
||||
var error = m.prop()
|
||||
|
||||
var prop = m.request({
|
||||
method: "GET",
|
||||
url: "test",
|
||||
deserialize: function () { throw new Error("error occurred") }
|
||||
}).catch(error)
|
||||
resolve()
|
||||
|
||||
expect(prop().message).to.equal("error occurred")
|
||||
expect(error().message).to.equal("error occurred")
|
||||
})
|
||||
|
||||
it("synchronously throws TypeErrors", function () {
|
||||
var error = m.prop()
|
||||
var exception
|
||||
var prop = m.request({
|
||||
method: "GET",
|
||||
url: "test",
|
||||
deserialize: function () { throw new TypeError("error occurred") }
|
||||
}).then(null, error)
|
||||
|
||||
try {
|
||||
resolve()
|
||||
} catch (e) {
|
||||
exception = e
|
||||
}
|
||||
|
||||
expect(prop()).to.not.exist
|
||||
expect(error()).to.not.exist
|
||||
expect(exception.message).to.equal("error occurred")
|
||||
})
|
||||
|
||||
it("sets correct Content-Type when given data", function () {
|
||||
var error = m.prop()
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
data: {foo: 1}
|
||||
}).then(null, error)
|
||||
|
||||
var xhr = mock.XMLHttpRequest.$instances.pop()
|
||||
xhr.onreadystatechange()
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"Content-Type",
|
||||
"application/json; charset=utf-8")
|
||||
})
|
||||
|
||||
it("doesn't set Content-Type when it doesn't have data", function () {
|
||||
var error = m.prop()
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "test"
|
||||
}).then(null, error)
|
||||
|
||||
var xhr = mock.XMLHttpRequest.$instances.pop()
|
||||
xhr.onreadystatechange()
|
||||
|
||||
expect(xhr.$headers).to.not.have.property("Content-Type")
|
||||
})
|
||||
|
||||
it("sets xhr request headers as per the headers config", function () {
|
||||
var error = m.prop()
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
headers: {
|
||||
"Authorization" : "Bearer 12345abcd12345",
|
||||
"CustomHeader" : "CustomValue"
|
||||
}
|
||||
}).then(null, error)
|
||||
|
||||
var xhr = mock.XMLHttpRequest.$instances.pop()
|
||||
xhr.onreadystatechange()
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"Authorization",
|
||||
"Bearer 12345abcd12345")
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"CustomHeader",
|
||||
"CustomValue")
|
||||
})
|
||||
|
||||
it("overwrites existing headers", function () {
|
||||
var error = m.prop()
|
||||
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
// Trigger the Content-Type addition
|
||||
data: {foo: "bar"},
|
||||
headers: {
|
||||
"Authorization" : "Bearer 12345abcd12345",
|
||||
"CustomHeader" : "CustomValue",
|
||||
"Content-Type" : "CustomType"
|
||||
}
|
||||
}).then(null, error)
|
||||
|
||||
var xhr = mock.XMLHttpRequest.$instances.pop()
|
||||
xhr.onreadystatechange()
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"Authorization",
|
||||
"Bearer 12345abcd12345")
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"CustomHeader",
|
||||
"CustomValue")
|
||||
|
||||
expect(xhr.$headers).to.have.property(
|
||||
"Content-Type",
|
||||
"CustomType")
|
||||
})
|
||||
|
||||
|
||||
it("correctly sets initial value", function () {
|
||||
var prop = m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
initialValue: "foo"
|
||||
})
|
||||
|
||||
var initialValue = prop()
|
||||
resolve()
|
||||
|
||||
expect(initialValue).to.equal("foo")
|
||||
})
|
||||
|
||||
it("correctly propagates initial value when not completed", function () {
|
||||
var prop = m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
initialValue: "foo"
|
||||
}).then(function (value) { return value })
|
||||
|
||||
var initialValue = prop()
|
||||
resolve()
|
||||
|
||||
expect(initialValue).to.equal("foo")
|
||||
})
|
||||
|
||||
it("resolves `then` correctly with an initialValue", function () {
|
||||
var prop = m.request({
|
||||
method: "POST",
|
||||
url: "test",
|
||||
initialValue: "foo"
|
||||
}).then(function () { return "bar" })
|
||||
|
||||
resolve()
|
||||
expect(prop()).to.equal("bar")
|
||||
})
|
||||
|
||||
it("appends query strings to `url` from `data` for `GET`", function () {
|
||||
var prop = m.request({method: "GET", url: "/test", data: {foo: 1}})
|
||||
resolve()
|
||||
expect(prop().url).to.equal("/test?foo=1")
|
||||
})
|
||||
|
||||
it("doesn't append query strings to `url` from `data` for `POST`", function () { // eslint-disable-line
|
||||
var prop = m.request({method: "POST", url: "/test", data: {foo: 1}})
|
||||
resolve()
|
||||
expect(prop().url).to.equal("/test")
|
||||
})
|
||||
|
||||
it("ignores interpolations without data", function () { // eslint-disable-line
|
||||
var prop = m.request({method: "GET", url: "/test:notfound", data: {foo: 1}})
|
||||
resolve()
|
||||
expect(prop().url).to.equal("/test:notfound?foo=1")
|
||||
})
|
||||
|
||||
it("appends children in query strings to `url` from `data` for `GET`", function () { // eslint-disable-line
|
||||
var prop = m.request({method: "GET", url: "test", data: {foo: [1, 2]}})
|
||||
resolve()
|
||||
expect(prop().url).to.equal("test?foo=1&foo=2")
|
||||
})
|
||||
|
||||
it("propagates initial value in call before request is completed", function () { // eslint-disable-line
|
||||
var value
|
||||
var prop1 = m.request({method: "GET", url: "test", initialValue: 123})
|
||||
expect(prop1()).to.equal(123)
|
||||
var prop2 = prop1.then(function () { return 1 })
|
||||
expect(prop2()).to.equal(123)
|
||||
var prop3 = prop1.then(function (v) { value = v })
|
||||
expect(prop3()).to.equal(123)
|
||||
resolve()
|
||||
|
||||
expect(value.method).to.equal("GET")
|
||||
expect(value.url).to.equal("test")
|
||||
})
|
||||
|
||||
context("over jsonp", function () {
|
||||
/* eslint-disable no-invalid-this */
|
||||
beforeEach(function () {
|
||||
var body = this.body = mock.document.createElement("body")
|
||||
mock.document.body = body
|
||||
mock.document.appendChild(body)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
mock.document.removeChild(this.body)
|
||||
})
|
||||
/* eslint-enable no-invalid-this */
|
||||
|
||||
function request(data, callbackKey) {
|
||||
return m.request({
|
||||
url: "/test",
|
||||
dataType: "jsonp",
|
||||
data: data,
|
||||
callbackKey: callbackKey
|
||||
})
|
||||
}
|
||||
|
||||
function find(list, item, prop) {
|
||||
var res
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var entry = list[i]
|
||||
if (prop != null) entry = entry[prop]
|
||||
if (entry.indexOf(item) >= 0) res = entry
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function resolve(data) {
|
||||
var callback = find(Object.keys(mock), "mithril_callback")
|
||||
var url = find(mock.document.getElementsByTagName("script"),
|
||||
callback, "src")
|
||||
mock[callback](data)
|
||||
return url
|
||||
}
|
||||
|
||||
it("sets the `GET` url with the correct query parameters", function () {
|
||||
request({foo: "bar"})
|
||||
expect(resolve({foo: "bar"})).to.contain("foo=bar")
|
||||
})
|
||||
|
||||
it("correctly gets the value, without appending the script on the document", function () { // eslint-disable-line
|
||||
var data = m.prop()
|
||||
|
||||
request().then(data)
|
||||
|
||||
var url = resolve({foo: "bar"})
|
||||
|
||||
expect(url).to.contain("/test?callback=mithril_callback")
|
||||
expect(data()).to.eql({foo: "bar"})
|
||||
})
|
||||
|
||||
it("correctly gets the value with a custom `callbackKey`, without appending the script on the document", function () { // eslint-disable-line
|
||||
var data = m.prop()
|
||||
|
||||
request(null, "jsonpCallback").then(data)
|
||||
|
||||
var url = resolve({foo: "bar1"})
|
||||
|
||||
expect(url).to.contain("/test?jsonpCallback=mithril_callback")
|
||||
expect(data()).to.eql({foo: "bar1"})
|
||||
})
|
||||
|
||||
it("correctly gets the value on calling the function", function () {
|
||||
var req = request()
|
||||
resolve({foo: "bar1"})
|
||||
expect(req()).to.eql({foo: "bar1"})
|
||||
})
|
||||
})
|
||||
|
||||
it("ends the computation when a SyntaxError is thrown from `options.extract`", function () { // eslint-disable-line max-len
|
||||
var root = mock.document.createElement("div")
|
||||
var viewSpy = sinon.spy(function () { return m("div") })
|
||||
var resolved = sinon.spy()
|
||||
var rejected = sinon.spy()
|
||||
|
||||
m.mount(root, {
|
||||
controller: function () {
|
||||
m.request({
|
||||
url: "/test",
|
||||
extract: function () {
|
||||
throw new SyntaxError()
|
||||
}
|
||||
}).then(resolved, rejected)
|
||||
},
|
||||
|
||||
view: viewSpy
|
||||
})
|
||||
|
||||
// For good measure
|
||||
mock.requestAnimationFrame.$resolve()
|
||||
|
||||
expect(function () {
|
||||
resolve()
|
||||
}).to.throw()
|
||||
|
||||
expect(resolved).to.not.have.been.called
|
||||
expect(rejected).to.not.have.been.called
|
||||
|
||||
// The controller should throw, but the view should still render.
|
||||
expect(viewSpy).to.have.been.called
|
||||
|
||||
// For good measure
|
||||
mock.requestAnimationFrame.$resolve()
|
||||
})
|
||||
|
||||
it("can use a config correctly", function () {
|
||||
var config = sinon.spy()
|
||||
var result = m.prop()
|
||||
var error = sinon.spy
|
||||
var opts = {
|
||||
method: "GET",
|
||||
url: "/test",
|
||||
config: config
|
||||
}
|
||||
m.request(opts).then(result, error)
|
||||
var xhr = resolve({foo: "bar"})
|
||||
|
||||
expect(config).to.be.calledWithExactly(xhr, opts)
|
||||
expect(result()).to.eql({foo: "bar"})
|
||||
expect(error).to.not.be.called
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,116 +0,0 @@
|
|||
describe("m.trust()", function () {
|
||||
"use strict"
|
||||
|
||||
it("exists", function () {
|
||||
expect(m.trust).to.be.a("function")
|
||||
})
|
||||
|
||||
it("returns an instance of String", function () {
|
||||
expect(m.trust("foo")).to.be.an.instanceof(String)
|
||||
})
|
||||
|
||||
it("does not modify the string", function () {
|
||||
expect(m.trust("foo").valueOf()).to.equal("foo")
|
||||
})
|
||||
|
||||
it("is not identical to the string", function () {
|
||||
expect(m.trust("foo")).to.not.equal("foo")
|
||||
})
|
||||
|
||||
// FIXME: implement document.createRange().createContextualFragment() in the
|
||||
// mock window for these tests
|
||||
dom(function () {
|
||||
it("isn't escaped in m.render()", function () {
|
||||
var root = document.createElement("div")
|
||||
m.render(root, m("div", "a", m.trust("&"), "b"))
|
||||
expect(root.childNodes[0].innerHTML).to.equal("a&b")
|
||||
})
|
||||
|
||||
it("works with mixed trusted content in div", function () {
|
||||
var root = document.createElement("div")
|
||||
m.render(root, [m.trust("<p>1</p><p>2</p>"), m("i", "foo")])
|
||||
expect(root.childNodes[2].tagName).to.equal("I")
|
||||
})
|
||||
|
||||
it("works with mixed trusted content in text nodes", function () {
|
||||
var root = document.createElement("div")
|
||||
m.render(root, [
|
||||
m.trust("<p>1</p>123<p>2</p>"),
|
||||
m("i", "foo")
|
||||
])
|
||||
expect(root.childNodes[3].tagName).to.equal("I")
|
||||
})
|
||||
|
||||
it("works with mixed trusted content in td", function () {
|
||||
var root = document.createElement("table")
|
||||
root.appendChild(root = document.createElement("tr"))
|
||||
|
||||
m.render(root, [
|
||||
m.trust("<td>1</td><td>2</td>"),
|
||||
m("td", "foo")
|
||||
])
|
||||
|
||||
expect(root.childNodes[2].tagName).to.equal("TD")
|
||||
})
|
||||
|
||||
it("works with trusted content in div", function () {
|
||||
var root = document.createElement("div")
|
||||
m.render(root, m("div", [
|
||||
m("p", "©"),
|
||||
m("p", m.trust("©")),
|
||||
m.trust("©")
|
||||
]))
|
||||
|
||||
expect(root.innerHTML)
|
||||
.to.equal("<div><p>&copy;</p><p>©</p>©</div>")
|
||||
})
|
||||
|
||||
// https://github.com/lhorie/mithril.js/issues/1045
|
||||
it("correctly injects script tags and executes them", function () {
|
||||
var HTMLString =
|
||||
"<script>document.getElementById('root').innerText='After'</script>"
|
||||
var root = document.createElement("div")
|
||||
var child = document.createElement("div")
|
||||
root.id = "root"
|
||||
root.innerText = "Before"
|
||||
root.appendChild(child)
|
||||
document.body.appendChild(root)
|
||||
|
||||
m.render(child, m.trust(HTMLString))
|
||||
|
||||
expect(root.innerText).to.equal("After")
|
||||
})
|
||||
|
||||
// https://github.com/lhorie/mithril.js/issues/956
|
||||
it("works with many and nested tags in trusted content", function () {
|
||||
var page = {
|
||||
names: m.prop(["John", "Paul", "George", "Ringo"]),
|
||||
nodeString: function (name) {
|
||||
return "<div><p>Hi </p></div><div><p>" + name + "</p></div>"
|
||||
},
|
||||
view: function () {
|
||||
return m("div",
|
||||
this.names().map(function (name) {
|
||||
return m.trust(this.nodeString(name))
|
||||
}, this)
|
||||
)
|
||||
}
|
||||
}
|
||||
m.render(document.body, page)
|
||||
var root = document.body.children[0]
|
||||
expect(root.children.length).to.equal(2 * page.names().length)
|
||||
for (var i = 0; i < page.names().length; i++) {
|
||||
var section = root.children[2 * i + 1]
|
||||
expect(section.children[0].innerText).to.equal(page.names()[i])
|
||||
}
|
||||
|
||||
page.names(["Jack", "Jill"])
|
||||
m.render(document.body, page)
|
||||
expect(root.children.length).to.equal(2 * page.names().length)
|
||||
for (i = 0; i < page.names().length; i++) {
|
||||
section = root.children[2 * i + 1]
|
||||
expect(section.children[0].innerText).to.equal(page.names()[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
<<<<<<< HEAD
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -16,20 +15,6 @@
|
|||
<script src="../test-utils/browserMock.js"></script>
|
||||
<script src="../mithril.js"></script>
|
||||
<script src="test-api.js"></script>
|
||||
=======
|
||||
<!doctype html>
|
||||
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.3/es5-shim.min.js"></script>-->
|
||||
<script src="test.js"></script>
|
||||
<script src="mock.js"></script>
|
||||
<script src="../mithril.js"></script>
|
||||
<script src="mithril-tests.js"></script>
|
||||
<script>
|
||||
// shim for running the old test suite with grunt-mocha-phantomjs like the new one
|
||||
if ('callPhantom' in window) {
|
||||
window.callPhantom({ testRunEnded: 1})
|
||||
}
|
||||
</script>
|
||||
>>>>>>> origin/next
|
||||
|
||||
<script>require("../ospec/ospec").run()</script>
|
||||
</body>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue