Some documentation
This commit is contained in:
parent
9bc6a64798
commit
cd8b8b12e6
6 changed files with 1073 additions and 0 deletions
238
docs/components.md
Normal file
238
docs/components.md
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
# Components
|
||||
|
||||
- [Structure](#structure)
|
||||
- [Lifecycle methods](#lifecycle-methods)
|
||||
- [State](#state)
|
||||
- [Avoid-anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
### Structure
|
||||
|
||||
Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.
|
||||
|
||||
Any Javascript object that has a view method is a Mithril component. Components can be consumed via the [`m`](hyperscript.md) utility:
|
||||
|
||||
```javascript
|
||||
var Example = {
|
||||
view: function() {
|
||||
return m("div", "Hello")
|
||||
}
|
||||
}
|
||||
|
||||
m(Example)
|
||||
|
||||
// equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Lifecycle methods
|
||||
|
||||
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove` and `shouldUpdate`.
|
||||
|
||||
```javascript
|
||||
var ComponentWithHooks = {
|
||||
oninit: function(vnode) {
|
||||
console.log("initialized")
|
||||
},
|
||||
oncreate: function(vnode) {
|
||||
console.log("DOM created")
|
||||
},
|
||||
onupdate: function(vnode) {
|
||||
console.log("DOM updated")
|
||||
},
|
||||
onbeforeremove: function(vnode, done) {
|
||||
console.log("exit animation can start")
|
||||
done()
|
||||
},
|
||||
onremove: function(vnode) {
|
||||
console.log("removing DOM element")
|
||||
},
|
||||
shouldUpdate: function(vnode, old) {
|
||||
return true
|
||||
},
|
||||
view: function(vnode) {
|
||||
return "hello"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
|
||||
|
||||
```javascript
|
||||
function initialize() {
|
||||
console.log("initialized as vnode")
|
||||
}
|
||||
|
||||
m(ComponentWithHooks, {oninit: initialize})
|
||||
```
|
||||
|
||||
Lifecycle methods in vnodes do not override component methods, nor vice versa. Component lifecycle methods are always run after the vnode's corresponding method.
|
||||
|
||||
To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md).
|
||||
|
||||
---
|
||||
|
||||
### State
|
||||
|
||||
Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns.
|
||||
|
||||
The state of a component can be accessed three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
|
||||
|
||||
#### At initialization
|
||||
|
||||
Any property attached to the component object is deep-cloned for every instance of the component. This allows simple state initialization.
|
||||
|
||||
In the example below, `data` is a property of the `Input` component's state object.
|
||||
|
||||
```javascript
|
||||
var ComponentWithInitialState = {
|
||||
data: "Initial content",
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.state.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithInitialState)
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Initial content</div>
|
||||
```
|
||||
|
||||
#### Via vnode.state
|
||||
|
||||
State can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component.
|
||||
|
||||
```javascript
|
||||
var ComponentWithDynamicState = {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.data = vnode.attrs.text
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.state.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithDynamicState, {text: "Hello"})
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
#### Via the this keyword
|
||||
|
||||
State can also be accessed via the `this` keyword, which is available to all lifecycle methods as well as the `view` method of a component.
|
||||
|
||||
```javascript
|
||||
var ComponentUsingThis = {
|
||||
oninit: function() {
|
||||
this.data = vnode.attrs.text
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("div", this.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentUsingThis, {text: "Hello"})
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
Be aware that when using ES5 functions, the value of `this` in nested anonymous functions is not the component instance. There are two recommended ways to get around this Javascript limitation, use ES6 arrow functions, of if ES6 is not available, use `vnode.state`.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
Although Mithril is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid restrictive interfaces
|
||||
|
||||
A component has a restrictive interface when it exposes only specific properties, under the assumption that other properties will not be needed, or that they can be added at a later time.
|
||||
|
||||
In the example below, the `button` configuration is severely limited: it does not support any events other than `onclick`, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML).
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var RestrictiveComponent = {
|
||||
view: function(vnode) {
|
||||
return m("button", {onclick: vnode.attrs.onclick}, [
|
||||
"Click to " + vnode.attrs.text
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It's preferable to allow passing through parameters to a component's root node, if it makes sense to do so:
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var FlexibleComponent = {
|
||||
view: function(vnode) {
|
||||
return m("button", vnode.attrs, [
|
||||
"Click to ", vnode.children
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Avoid magic indexes
|
||||
|
||||
Often it's desireable to define multiple sets of children, for example, if a component has a configurable title and body.
|
||||
|
||||
Avoid destructuring the `children` property for this purpose.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var Header = {
|
||||
view: function(vnode) {
|
||||
return m(".section", [
|
||||
m(".header", vnode.children[0]),
|
||||
m(".tagline", vnode.children[1]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m(Header, [
|
||||
m("h1", "My title"),
|
||||
m("h2", "Lorem ipsum"),
|
||||
])
|
||||
|
||||
// awkward consumption use case
|
||||
m(Header, [
|
||||
[
|
||||
m("h1", "My title"),
|
||||
m("small", "A small note"),
|
||||
],
|
||||
m("h2", "Lorem ipsum"),
|
||||
])
|
||||
```
|
||||
|
||||
The component above makes different children look different based on where they appear in the array. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content:
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var BetterHeader = {
|
||||
view: function(vnode) {
|
||||
return m(".section", [
|
||||
m(".header", vnode.attrs.title),
|
||||
m(".tagline", vnode.attrs.tagline),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
m(BetterHeader, {
|
||||
title: m("h1", "My title"),
|
||||
tagline: m("h2", "Lorem ipsum"),
|
||||
})
|
||||
|
||||
// clearer consumption use case
|
||||
m(Header, {
|
||||
title: [
|
||||
m("h1", "My title"),
|
||||
m("small", "A small note"),
|
||||
],
|
||||
tagline: m("h2", "Lorem ipsum"),
|
||||
})
|
||||
```
|
||||
412
docs/hyperscript.md
Normal file
412
docs/hyperscript.md
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
# m(selector, attributes, children)
|
||||
|
||||
- [Signature](#signature)
|
||||
- [How it works](#how-it-works)
|
||||
- [Flexibility](#flexibility)
|
||||
- [CSS selectors](#css-selectors)
|
||||
- [DOM attributes](#dom-attributes)
|
||||
- [Style attribute](#style-attribute)
|
||||
- [Events](#events)
|
||||
- [Properties](#properties)
|
||||
- [Components](#components)
|
||||
- [Lifecycle methods](#lifecycle-methods)
|
||||
- [Keys](#keys)
|
||||
- [SVG and MathML](#svg-and-mathml)
|
||||
- [Making templates dynamic](#making-templates-dynamic)
|
||||
- [Avoid-anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### Signature
|
||||
|
||||
`vnode = m(selector, attributes, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------------ | -------- | ---
|
||||
`selector` | `String|Object` | Yes | A CSS selector or a component
|
||||
`attributes` | `Object` | No | HTML attributes or element properties
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats)
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md#structure)
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
Mithril provides a hyperscript function `m`, which allows expressing any HTML structure using javascript syntax. It accepts a `selector` string (required), an `attributes` object (optional) and a `children` array (optional).
|
||||
|
||||
```javascript
|
||||
var m = require("mithril")
|
||||
|
||||
m("div", {id: "box"}, "hello")
|
||||
|
||||
// equivalent HTML:
|
||||
// <div id="box">hello</div>
|
||||
```
|
||||
|
||||
The `m` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a javascript object that represents the DOM element to be created.
|
||||
|
||||
```javascript
|
||||
//a vnode
|
||||
{tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}
|
||||
```
|
||||
|
||||
To transform a vnode into an actual DOM element, use the [`m.render()`](render.md) function:
|
||||
|
||||
```
|
||||
m.render(document.body, m("br")) // puts a <br> in <body>
|
||||
```
|
||||
|
||||
Calling `m.render()` multiple times does **not** recreate the DOM tree from scratch each time. Instead, each call will only make a change to a DOM tree if it is absolutely necessary to reflect the virtual DOM tree passed into the call. This behavior is desirable because recreating the DOM from scratch is very expensive, and causes issues such as loss of input focus, among other things. By contrast, updating the DOM only where necessary is comparatively much faster and makes it easier to maintain complex UIs that handle multiple user stories.
|
||||
|
||||
---
|
||||
|
||||
### Flexibility
|
||||
|
||||
The `m` function is both *polymorphic* and *variadic*. In other words, it's very flexible in what it expects as input parameters:
|
||||
|
||||
```javascript
|
||||
//simple tag
|
||||
m("div") // <div></div>
|
||||
|
||||
//attributes and children are optional
|
||||
m("a", {id: "b"}) // <a id="b"></a>
|
||||
m("span", "hello") // <span>hello</span>
|
||||
|
||||
//tag with child nodes
|
||||
m("ul", [ // <ul>
|
||||
m("li", "hello"), // <li>hello</li>
|
||||
m("li", "world"), // <li>world</li>
|
||||
]) // </ul>
|
||||
|
||||
// array is optional
|
||||
m("ul", // <ul>
|
||||
m("li", "hello"), // <li>hello</li>
|
||||
m("li", "world") // <li>world</li>
|
||||
) // </ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CSS selectors
|
||||
|
||||
The first argument of `m` can be any CSS selector that can describe an HTML element. It accepts any valid CSS combinations of `#` (id), `.` (class) and `[]` (attribute) syntax.
|
||||
|
||||
```javascript
|
||||
m("div#hello")
|
||||
// <div id="hello"></div>
|
||||
|
||||
m("section.container")
|
||||
// <section class="container"></section>
|
||||
|
||||
m("input[type=text][placeholder=Name]")
|
||||
// <input type="text" placeholder="Name" />
|
||||
|
||||
m("a#exit.external[href='http://example.com']", "Leave")
|
||||
// <a id="exit" class="external" href="http://example.com">Leave</a>
|
||||
```
|
||||
|
||||
If you omit the tag name, Mithril assumes a `div` tag.
|
||||
|
||||
```javascript
|
||||
m(".box.box-bordered") // <div class="box box-bordered"></div>
|
||||
```
|
||||
|
||||
Typically, it's recommended that you use CSS selectors for static attributes (i.e. attributes whose value do not change), and pass an attributes object for dynamic attribute values.
|
||||
|
||||
```javascript
|
||||
var currentURL = "/"
|
||||
|
||||
m("a.link[href=/]", {
|
||||
class: currentURL === "/" ? "selected" : ""
|
||||
}, "Home")
|
||||
|
||||
//equivalent HTML:
|
||||
<a href="/" class="link selected">Home</a>
|
||||
```
|
||||
|
||||
If there are class names in both first and second arguments of `m`, they are merged together as you would expect.
|
||||
|
||||
---
|
||||
|
||||
### DOM attributes
|
||||
|
||||
Mithril uses both the Javascript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes.
|
||||
|
||||
For example, in the Javascript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril, all of the following are supported:
|
||||
|
||||
```javascript
|
||||
m("input", {readonly: true}) //lowercase
|
||||
m("input", {readOnly: true}) //uppercase
|
||||
m("input[readonly]")
|
||||
m("input[readOnly]")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Style attribute
|
||||
|
||||
Mithril supports both strings and objects as valid `style` values. In other words, all of the following are supported:
|
||||
|
||||
```javascript
|
||||
m("div", {style: "background:red;"})
|
||||
m("div", {style: {background: "red"}})
|
||||
m("div[style=background:red")
|
||||
```
|
||||
|
||||
Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.
|
||||
|
||||
Mithril does not attempt to add units to number values.
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
Mithril supports event handler binding for all DOM events, including events whose specs do not define an `on<event>` property, such as `touchstart`
|
||||
|
||||
```
|
||||
function doSomething(e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
m("div", {onclick: doSomething})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Properties
|
||||
|
||||
Mithril supports DOM functionality that is accessible via properties such as `<select>`'s `selectedIndex` and `value` properties.
|
||||
|
||||
```javascript
|
||||
m("select", {selectedIndex: 0}, [
|
||||
m("option", "Option A"),
|
||||
m("option", "Option B"),
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SVG and MathML
|
||||
|
||||
Mithril fully supports SVG. Xlink is also supported, but unlike in pre-v1.0 versions of Mithril, must have the namespace explicitly defined:
|
||||
|
||||
```
|
||||
m("svg", [
|
||||
m("image[xlink:href='image.gif']")
|
||||
])
|
||||
```
|
||||
|
||||
MathML is also fully supported.
|
||||
|
||||
---
|
||||
|
||||
### Components
|
||||
|
||||
[Components](components.md) allow you to encapsulate logic into a unit and use it as if it was an element. They are the base for making large, scalable applications.
|
||||
|
||||
A component is any Javascript object that contains a `view` method. To consume a component, pass the component as the first argument to `m` instead of passing a CSS selector string. You can pass arguments to the component by defining attributes and children, as shown in the example below.
|
||||
|
||||
```javascript
|
||||
// define a component
|
||||
var Greeter = {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs, ["Hello ", vnode.children])
|
||||
}
|
||||
}
|
||||
|
||||
// consume it
|
||||
m(Greeter, {style: "color:red;"}, "world")
|
||||
|
||||
// equivalent HTML:
|
||||
// <div style="color:red;">Hello world</div>
|
||||
```
|
||||
|
||||
To learn more about components, [see the components page](components.md).
|
||||
|
||||
---
|
||||
|
||||
### Lifecycle methods
|
||||
|
||||
Vnodes and components can have lifecycle methods (also known as *hooks*), which are called at various points during the lifetime of a DOM element. The lifecycle methods supported by Mithril are: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove`, and `shouldUpdate`.
|
||||
|
||||
Lifecycle methods are defined in the same way as DOM event handlers, but receive the vnode as an argument, instead of an Event object:
|
||||
|
||||
```javascript
|
||||
function initialize(vnode) {
|
||||
console.log(vnode)
|
||||
}
|
||||
|
||||
m("div", {oninit: initialize})
|
||||
```
|
||||
|
||||
Hook | Description
|
||||
----------------------------- | ---
|
||||
`oninit(vnode)` | Runs before a vnode is rendered into a real DOM element
|
||||
`oncreate(vnode)` | Runs after a vnode is appended to the DOM
|
||||
`onupdate(vnode)` | Runs every time a redraw occurs while the DOM element is attached to the document
|
||||
`onbeforeremove(vnode, done)` | Runs before a DOM element is removed from the document, and only triggers the actual removal of the DOM element when the `done` callback is called. This method is only triggered on the element that is detached from its parent DOM element, but not on its child elements.
|
||||
`onremove(vnode)` | Runs before a DOM element is removed from the document. If a `onbeforeremove` hook is defined, `onremove` is called after `done` is called. This method is triggered on the element that is detached from its parent element, and all of its children
|
||||
`shouldUpdate(vnode, old)` | Runs before `onupdate` and if it returns `true`, it prevents a diff for the element and all of its children
|
||||
|
||||
To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md).
|
||||
|
||||
---
|
||||
|
||||
### Keys
|
||||
|
||||
Vnodes in a list can have a special attribute called `key`, which can be used to manage the identity of the DOM element as the model data that generates the vnode list changes.
|
||||
|
||||
Typically, `key` should be the unique identifier field of the objects in the data array.
|
||||
|
||||
```javascript
|
||||
var users = [
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Mary"},
|
||||
]
|
||||
|
||||
function userInputs(users) {
|
||||
return users.map(function(u) {
|
||||
return m("input", {key: u.id}, u.name)
|
||||
})
|
||||
}
|
||||
|
||||
m.render(document.body, userInputs(users))
|
||||
```
|
||||
|
||||
Having a key means that if the `users` array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.
|
||||
|
||||
To learn more about keys, [see the keys page](keys.md)
|
||||
|
||||
---
|
||||
|
||||
### Making templates dynamic
|
||||
|
||||
Since nested vnodes are just plain Javascript expressions, you can simply use Javascript facilities to manipulate them
|
||||
|
||||
#### Dynamic text
|
||||
|
||||
```javascript
|
||||
var user = {name: "John"}
|
||||
|
||||
m(".name", user.name) // <div class="name">John</div>
|
||||
```
|
||||
|
||||
#### Loops
|
||||
|
||||
Use `Array` methods such as [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to iterate over lists of data
|
||||
|
||||
```javascript
|
||||
var users = [
|
||||
{name: "John"},
|
||||
{name: "Mary"},
|
||||
]
|
||||
|
||||
m("ul", users.map(function(u) { // <ul>
|
||||
return m("li", u.name) // <li>John</li>
|
||||
// <li>Mary</li>
|
||||
})) // </ul>
|
||||
|
||||
// ES6:
|
||||
// m("ul", users.map(u =>
|
||||
// m("li", u.name)
|
||||
// ))
|
||||
```
|
||||
|
||||
#### Conditionals
|
||||
|
||||
Use the ternary operator to conditionally set content on a view
|
||||
|
||||
```javascript
|
||||
var isError = false
|
||||
|
||||
m("div", isError ? "An error occurred" : "Saved") // <div>Saved</div>
|
||||
```
|
||||
|
||||
You cannot use Javascript statements such as `if` or `for` within Javascript expressions. It's preferable to avoid using those statements altogether and instead, use the constructs above exclusively in order to keep the structure of the templates linear and declarative, and to avoid deoptimizations.
|
||||
|
||||
---
|
||||
|
||||
### Anti-patterns
|
||||
|
||||
Although Mithril is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid dynamic selectors
|
||||
|
||||
Different DOM elements have different attributes, and often different behaviors. Making a selector configurable can leak the implementation details of a component out of its unit.
|
||||
|
||||
```javascript
|
||||
// AVOID
|
||||
var BadInput = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label"),
|
||||
m(vnode.attrs.type || "input")
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead of making selectors dynamic, you are encouraged to explicitly code each valid possibility, or refactor the variable portion of the code out.
|
||||
|
||||
```javascript
|
||||
// PREFER explicit code
|
||||
var BetterInput = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
m("input"),
|
||||
])
|
||||
}
|
||||
}
|
||||
var BetterSelect = {
|
||||
view: function(vnode) {
|
||||
return m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
m("select"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// PREFER refactor variability out
|
||||
var BetterLabeledComponent = {
|
||||
view: function(vnode) {
|
||||
m("div", [
|
||||
m("label", vnode.attrs.title),
|
||||
vnode.children,
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Avoid statements in view methods
|
||||
|
||||
Javascript statements often require changing the naturally nested structure of an HTML tree, making the code more verbose and harder to understand. Constructing an virtual DOM tree procedurally can also potentially trigger expensive deoptimizations (such as an entire template being recreated from scratch)
|
||||
|
||||
```
|
||||
// AVOID
|
||||
var BadListComponent = {
|
||||
view: function(vnode) {
|
||||
var list = []
|
||||
for (var i = 0; i < vnode.attrs.items.length; i++) {
|
||||
list.push(m("li", vnode.attrs.items[i]))
|
||||
}
|
||||
|
||||
return m("ul", list)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead, prefer using Javascript expressions such as the ternary operator and Array methods.
|
||||
|
||||
```javascript
|
||||
// PREFER
|
||||
var BetterListComponent = {
|
||||
view: function() {
|
||||
return m("ul", vnode.attrs.items.map(function(item) {
|
||||
return m("li", item)
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
80
docs/keys.md
Normal file
80
docs/keys.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Keys
|
||||
|
||||
- [What are keys](#what-are-keys)
|
||||
- [How to use](#how-to-use)
|
||||
|
||||
---
|
||||
|
||||
### What are keys
|
||||
|
||||
Keys are a mechanism that allows re-ordering DOM elements within a NodeList, and mapping specific data items in a list to the respective DOM elements that are derived from them, as the data items move within the list.
|
||||
|
||||
In other words, a `key` is a way of saying "this DOM element is for the data object with this id".
|
||||
|
||||
Typically, `key` should be the unique identifier field of the objects in the data array.
|
||||
|
||||
```javascript
|
||||
var users = [
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Mary"},
|
||||
]
|
||||
|
||||
function userInputs(users) {
|
||||
return users.map(function(u) {
|
||||
return m("input", {key: u.id}, u.name)
|
||||
})
|
||||
}
|
||||
|
||||
m.render(document.body, userInputs(users))
|
||||
```
|
||||
|
||||
Having a key means that if the `users` array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.
|
||||
|
||||
---
|
||||
|
||||
### How to use
|
||||
|
||||
A common pattern is to have data comprised of an array of objects and to generate a list of vnodes that map to each object in the array. For example, consider the following code:
|
||||
|
||||
```javascript
|
||||
var people = [
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Mary"},
|
||||
]
|
||||
|
||||
function userList(users) {
|
||||
return users.map(function(u) {
|
||||
return m("button", u.name) // <button>John</button>
|
||||
// <button>Mary</button>
|
||||
})
|
||||
}
|
||||
|
||||
m.render(document.body, userList(people))
|
||||
```
|
||||
|
||||
Let's suppose the `people` variable was changed to this:
|
||||
|
||||
```javascript
|
||||
people = [{id: 2, name: "Mary"}]
|
||||
```
|
||||
|
||||
The problem is that from the point of view of the `userList` function, there's no way to tell if it was the first object that was removed, or if it was the second object that was removed *in addition to the first object's properties being modified*. If the first button was focused and the rendering engine removes it, then focus goes back to `<body>` as expected, but if the rendering engine removes the second button and modifies the text content of the first, then the focus will be on the wrong button after the update.
|
||||
|
||||
Worse still, if there were stateful jQuery plugins attached to these buttons, they could potentially have incorrect internal state after the update.
|
||||
|
||||
Even though in this particular example, we humans intuitively guess that the first item in the list was the one being removed, it's actually impossible for a computer to automatically solve this problem for all possible inputs.
|
||||
|
||||
Therefore, in the cases when a list of vnodes is derived from a *mutable* array of data, you should add a `key` property to each virtual node that maps to a uniquely identifiable field in the source data. This will allow Mithril to intelligently re-order the DOM to maintain each DOM element correctly mapped to its respective item in the data source.
|
||||
|
||||
```javascript
|
||||
function correctUserList(users) {
|
||||
return users.map(function(u) {
|
||||
return m("button", {key: u.id}, u.name)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Keys must be strings if present or they will be cast to strings if they are not. Therefore, `"1"` (string) and `1` (number) are considered the same key.
|
||||
|
||||
In addition, keys must be placed on the virtual node that is an immediate child of the array. This means that if you wrap the `button` in an `div` in the example above, the key must be moved to the `div`. Likewise, if you refactor the code and put the button inside a component, the key must be moved out of the component and placed back where the component took place of the button.
|
||||
|
||||
207
docs/lifecycle-methods.md
Normal file
207
docs/lifecycle-methods.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# Lifecycle methods
|
||||
|
||||
- [Usage](#usage)
|
||||
- [The DOM element lifecycle](#the-dom-element-lifecycle)
|
||||
- [oninit](#oninit)
|
||||
- [oncreate](#oncreate)
|
||||
- [onupdate](#onupdate)
|
||||
- [onbeforeremove](#onbeforeremove)
|
||||
- [onremove](#onremove)
|
||||
- [shouldUpdate](#shouldUpdate)
|
||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
### Usage
|
||||
|
||||
[Components](components.md) and [virtual DOM nodes](vnodes.md) and can have lifecycle methods, also known as *hooks*, which are called at various points during the lifetime of a DOM element.
|
||||
|
||||
```javascript
|
||||
// Sample hook in component
|
||||
var ComponentWithHook = {
|
||||
oninit: function(vnode) {
|
||||
console.log("initialize component")
|
||||
},
|
||||
view: function() {
|
||||
return "hello"
|
||||
}
|
||||
}
|
||||
|
||||
//Sample hook in vnode
|
||||
function initializeVnode() {
|
||||
console.log("initialize vnode")
|
||||
}
|
||||
|
||||
m(ComponentWithHook, {oninit: initializeVnode}})
|
||||
```
|
||||
|
||||
All lifecyle methods receive the vnode as their first arguments, and have their `this` keyword bound to `vnode.state`.
|
||||
|
||||
Lifecycle methods are only called as a side effect of a [`m.render()`](render.md) call. They are not called if the DOM is modified outside of Mithril.
|
||||
|
||||
---
|
||||
|
||||
### The DOM element lifecycle
|
||||
|
||||
A DOM element is typically created and appended to the document. It may then have attributes or child nodes updated when a UI event is triggered and data is changed; and the element may alternatively be removed from the document.
|
||||
|
||||
After an element is removed, it may be temporarily retained in a memory pool. The pooled element may be reused in a subsequent update (in a process called *DOM recycling*). Recycling an element avoids incurring the performance cost of recreating a copy of an element that existed recently.
|
||||
|
||||
---
|
||||
|
||||
### oninit
|
||||
|
||||
The `oninit(vnode)` hook is called before a vnode is touched by the virtual DOM engine. `oninit` is guaranteed to run before its DOM element is attached to the document, and it is guaranteed to run on parent vnodes before their children, but it does not offer any guarantees regarding the existence of ancestor or descendant DOM elements. You should never access the `vnode.dom` from the `oninit` method.
|
||||
|
||||
This hook does not get called when an element is updated, but it does get called if an element is recycled.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `oninit` callback points to `vnode.state`.
|
||||
|
||||
The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`.
|
||||
|
||||
```javascript
|
||||
var ComponentWithState = {
|
||||
oninit: function(vnode) {
|
||||
this.data = vnode.attrs.data
|
||||
},
|
||||
view: function() {
|
||||
return m("div", this.data)
|
||||
}
|
||||
}
|
||||
|
||||
m(ComponentWithState, {data: "Hello"})
|
||||
|
||||
// Equivalent HTML
|
||||
// <div>Hello</div>
|
||||
```
|
||||
|
||||
You should not modify model data synchronously from this method. Since `oninit` makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle.
|
||||
|
||||
---
|
||||
|
||||
### oncreate
|
||||
|
||||
The `oncreate(vnode)` hook is called after a DOM element is created and attached to the document. `oncreate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method.
|
||||
|
||||
This hook does not get called when an element is updated, but it does get called if an element is recycled.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `oncreate` callback points to `vnode.state`. DOM elements whose vnodes have an `oncreate` hook do not get recycled.
|
||||
|
||||
The `oncreate` hook is useful for reading layout values that may trigger a repaint, starting animations and for initializing third party libraries that require a reference to the DOM element.
|
||||
|
||||
```javascript
|
||||
var HeightReporter = {
|
||||
oncreate: function(vnode) {
|
||||
console.log("Initialized with height of: ", vnode.dom.offsetHeight)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
|
||||
m(HeightReporter, {data: "Hello"})
|
||||
```
|
||||
|
||||
You should not modify model data synchronously from this method. Since `oncreate` is run at the end of the render cycle, model changes created from this method will not be reflected in the UI until the next render cycle.
|
||||
|
||||
---
|
||||
|
||||
### onupdate
|
||||
|
||||
The `oncreate(vnode)` hook is called after a DOM element is updated, while attached to the document. `onupdate` is guaranteed to run at the end of the render cycle, so it is safe to read layout values such as `vnode.dom.offsetHeight` and `vnode.dom.getBoundingClientRect()` from this method.
|
||||
|
||||
This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onupdate` callback points to `vnode.state`. DOM elements whose vnodes have an `onupdate` hook do not get recycled.
|
||||
|
||||
The `onupdate` hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed.
|
||||
|
||||
```javascript
|
||||
var RedrawReporter = {
|
||||
count: 0,
|
||||
onupdate: function(vnode) {
|
||||
console.log("Redraws so far: ", ++vnode.state.count)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
|
||||
m(RedrawReporter, {data: "Hello"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### onbeforeremove
|
||||
|
||||
The `onbeforeremove(vnode, done)` hook is called before a DOM element is detached from the document. Mithril only detaches the DOM element after the `done` callback is called. The `done` callback can be called asynchronously, making it possible to run exit animations before detaching the element.
|
||||
|
||||
This hook is only called on the DOM element that loses its `parentNode`, but it does not get called in its child elements.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onbeforeremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onbeforeremove` hook do not get recycled.
|
||||
|
||||
```javascript
|
||||
var Fader = {
|
||||
onbeforeremove: function(vnode, done) {
|
||||
vnode.dom.classList.add("fade-out")
|
||||
setTimeout(done, 1000)
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "Bye")
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### onremove
|
||||
|
||||
The `onremove(vnode)` hook is called before a DOM element is removed from the document. If a `onbeforeremove` hook is also defined, the `onremove` hook runs after the `done` callback is called.
|
||||
|
||||
This hook is called on any element that is removed from the document, regardless of whether it was directly detached from its parent or whether it is a child of another element that was detached.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `onremove` callback points to `vnode.state`. DOM elements whose vnodes have an `onremove` hook do not get recycled.
|
||||
|
||||
The `onremove` hook is useful for running clean up tasks.
|
||||
|
||||
```javascript
|
||||
var Timer = {
|
||||
oninit: function(vnode) {
|
||||
this.timeout = setTimeout(function() {
|
||||
console.log("timed out")
|
||||
}, 1000)
|
||||
},
|
||||
onremove: function(vnode) {
|
||||
clearTimeout(this.timeout)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### shouldUpdate
|
||||
|
||||
The `shouldUpdate(vnode, old)` hook is called before a vnode is diffed in a update. If this function is defined and returns false, Mithril prevents a diff from happening to the vnode, and consequently to the vnode's children.
|
||||
|
||||
This hook by itself does not prevent a virtual DOM subtree from being generated unless the subtree is encapsulated within a component.
|
||||
|
||||
Like in other hooks, the `this` keyword in the `shouldUpdate` callback points to `vnode.state`.
|
||||
|
||||
This hook is useful to reduce lag in updates in cases where there is a overly large DOM tree.
|
||||
|
||||
---
|
||||
|
||||
### Avoid anti-patterns
|
||||
|
||||
Although Mithril is flexible, some code patterns are discouraged:
|
||||
|
||||
#### Avoid premature optimizations
|
||||
|
||||
The `shouldUpdate` hook should only be used as a last resort. Avoid using it unless you have a noticeable performance issue.
|
||||
|
||||
Typically performance problems that can be fixed via `shouldUpdate` boil down to one large array of items. In this context, typically "large" means any array that contains a large number of nodes, be it in a wide spread (the infamous 5000 row table), or in a deep, dense tree.
|
||||
|
||||
If you do have a performance issue, first consider whether the UI presents a good user experience and change it if it doesn't. For example, it's highly unlikely that a user would ever sift through 5000 rows of raw table data, and highly likely that it would be easier for a user to use a search feature that returns only the top few most relevant items.
|
||||
|
||||
If a design-based solution is not feasible, and you must optimize a UI with a large number of DOM element, apply `shouldUpdate` on the parent node of the largest array and re-evaluate performance. In the vast majority of cases, a single check should be sufficient. In the rare case that it is not, rinse and repeat, but you should be increasingly wary of each new `shouldUpdate` declaration. Multiple `shouldUpdate`s are a code smell that indicates prioritization problems in the design workflow.
|
||||
|
||||
Avoid applying the optimization to other areas of your application "just-in-case". Remember that, generally speaking, more code incurs a higher maintenance cost than less code, and `shouldUpdate` related bugs can be especially difficult to troubleshoot if you rely on object identity for its conditional checks.
|
||||
|
||||
Again, **the `shouldUpdate` hook should only be used as a last resort.**
|
||||
34
docs/signatures.md
Normal file
34
docs/signatures.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# How to read signatures
|
||||
|
||||
Signature sections typically look like this:
|
||||
|
||||
`vnode = m(selector, attributes, children)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | ------------------------------------ | -------- | ---
|
||||
`selector` | `String|Object` | Yes | A CSS selector or a component
|
||||
`attributes` | `Object` | No | HTML attributes or element properties
|
||||
`children` | `Array<Vnode>|String|Number|Boolean` | No | Child [vnodes](vnodes.md). Can be written as [splat arguments](signatures.md#splats)
|
||||
**returns** | `Vnode` | | A [vnode](vnodes.md)
|
||||
|
||||
The signature line above the table indicates the general syntax of the method, showing the name of the method, the order of its arguments and a suggested variable name for its return value.
|
||||
|
||||
The **Argument** column in the table indicates which part of the signature is explained by the respective table row. The `returns` row displays information about the return value of the method.
|
||||
|
||||
The **Type** column indicates the expected type for the argument.
|
||||
|
||||
A pipe (`|`) indicates that an argument is valid if it has any of the listed types. For example, `String|Object` indicates that `selector` can be a string OR an object.
|
||||
|
||||
Angled brackets (`< >`) after an `Array` indicate the expected type for array items. For exampe, `Array<String>` indicates that an argument must be an array and that all items in that array must be strings.
|
||||
|
||||
Sometimes non-native types may appear to indicate that a specific object signature is required. For example, `Vnode` is an object that has a [virtual DOM node](vnodes.md) structure.
|
||||
|
||||
The **Required** column indicates whether an argument is required or optional. If an argument is optional, you may set it to `null` or `undefined`, or omit it altogether, such that the next argument appears in its place.
|
||||
|
||||
### Splats
|
||||
|
||||
A splat argument means that if the last argument is an array, you can omit the square brackets and have a variable number of arguments in the method instead.
|
||||
|
||||
In the example at the top, this means that `m("div", {id: "foo"}, ["a", "b", "c"])` can also be written as `m("div", {id: "foo"}, "a", "b", "c")`.
|
||||
|
||||
Splats are useful in some compile-to-js languages such as Coffeescript, and also allow helpful shorthands for some common use cases.
|
||||
102
docs/vnodes.md
Normal file
102
docs/vnodes.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Virtual DOM nodes
|
||||
|
||||
- [What is virtual DOM](#what-is-virtual-dom)
|
||||
- [Basics](#basics)
|
||||
- [Structure](#structure)
|
||||
- [Vnode types](#vnode-types)
|
||||
- [Monomorphic class](#monomorphic-class)
|
||||
|
||||
---
|
||||
|
||||
### What is virtual DOM
|
||||
|
||||
A virtual DOM tree is a Javascript data structure that describes a DOM tree. It consists of nested virtual DOM nodes, also known *vnodes*.
|
||||
|
||||
The first time a virtual DOM tree is rendered, it is used as a blueprint to create a DOM tree that matches its structure.
|
||||
|
||||
Typically, Virtual DOM trees are then recreated every render cycle, which normally occurs in response to event handlers or to data changes. Mithril *diffs* a vnode tree against its previous version and only modifies DOM elements in spots where there are changes.
|
||||
|
||||
It may seem wasteful to recreate vnodes so frequently, but as it turns out, modern Javascript engines can create hundres of thousands of objects in less than a millisecond. On the other hand, modifying the DOM is several orders of magnitude more expensive than creating vnodes.
|
||||
|
||||
For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well.
|
||||
|
||||
The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide [retained mode rendering](https://en.wikipedia.org/wiki/Retained_mode), a style of rendering that makes it drastically easier to manage UI complexity.
|
||||
|
||||
To illustrate why retained mode is so important, consider the DOM API and HTML. The DOM API is an [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics)) rendering system and requires writing out exact instructions to assemble a DOM tree procedurally. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances for introducing bugs and more chances to make code harder to understand.
|
||||
|
||||
In contrast, HTML is a retained mode rendering system. With HTML, you can write a DOM tree in a far more natural and readable way, without worrying about forgetting to append a child to a parent, running into stack overflows when rendering extremely deep trees, etc.
|
||||
|
||||
Virtual DOM goes one step further than HTML by allowing you to write *dynamic* DOM trees without having to manually write multiple sets of DOM API calls to efficiently synchronize the UI to arbitrary data changes.
|
||||
|
||||
---
|
||||
|
||||
### Basics
|
||||
|
||||
Virtual DOM nodes, or *vnodes*, are javascript objects that represent DOM elements (or parts of the DOM). Mithril's virtual DOM engine consumes a tree of vnodes to produce a DOM tree.
|
||||
|
||||
Vnodes can be created via the [`m`](hyperscript.md) hyperscript utility:
|
||||
|
||||
```javascript
|
||||
m("div", {id: "test"}, "hello")
|
||||
```
|
||||
|
||||
Vnodes can also consume [components](components.md):
|
||||
|
||||
```javascript
|
||||
// define a component
|
||||
var ExampleComponent = {
|
||||
view: function(vnode) {
|
||||
return m("div", vnode.attrs, ["Hello ", vnode.children])
|
||||
}
|
||||
}
|
||||
|
||||
// consume it
|
||||
m(ExampleComponent, {style: "color:red;"}, "world")
|
||||
|
||||
// equivalent HTML:
|
||||
// <div style="color:red;">Hello world</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Structure
|
||||
|
||||
Virtual DOM nodes, or *vnodes*, are Javascript objects that represent an element (or parts of the DOM) and have the following properties:
|
||||
|
||||
Property | Type | Description
|
||||
---------- | -------------------------------- | ---
|
||||
`tag` | `String|Object` | The `nodeName` of a DOM element. It may also be the string `[` if a vnode is a fragment, `#` if it's a text vnode, or `<` if it's a trusted HTML vnode. Additionally, it may be a component.
|
||||
`key` | `String?` | The value used to map a DOM element to its respective item in a array of data.
|
||||
`attrs` | `Object?` | A hashmap of [DOM attributes](hyperscript.md#dom-attributes), [events](hyperscript.md#events), [properties](hyperscript.md#properties) and [lifecycle methods](hyperscript.md#lifecycle-methods).
|
||||
`children` | `(Array|String|Number|Boolean)?` | In most vnode types, the `children` property is an array of vnodes. For text and trusted HTML vnodes, The `children` property is either a string, a number or a boolean.
|
||||
`text` | `(String|Number|Boolean)?` | This is used instead of `children` if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the `text` property even if they have a text node as its only child.
|
||||
`dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragment and trusted HTML vnodes, `dom` points to the first element in the range.
|
||||
`domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property).
|
||||
`state` | `Object` | An object that is persisted between redraws. In component vnodes, `state` is a deep clone of the component object.
|
||||
`events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use it.
|
||||
|
||||
---
|
||||
|
||||
### Vnode types
|
||||
|
||||
The `tag` property of a vnode determines its type. There are five vnode types:
|
||||
|
||||
Vnode type | Example | Description
|
||||
------------ | ------------------------------ | ---
|
||||
Element | `{tag: "div"}` | Represents a DOM element.
|
||||
Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment.
|
||||
Text | `{tag: "#", children: ""}` | Represents a DOM text node.
|
||||
Trusted HTML | `{tag: "<", children: "<br>"}` | Represents a list of DOM elements from an HTML string.
|
||||
Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component.
|
||||
|
||||
Everything in a virtual DOM tree is a vnode, including text. The `m()` utility automatically normalizes its `children` argument and turns strings into text vnodes and nested arrays into fragment vnodes.
|
||||
|
||||
Only element tag names and components can be the first argument of the `m` function. In other words, `[`, `#` and `<` are not valid `selector` arguments for `m()`. Trusted HTML vnodes can be created via [`m.trust()`](trust.md)
|
||||
|
||||
---
|
||||
|
||||
### Monomorphic class
|
||||
|
||||
The `mithril/render/node` module is used by Mithril to generate all vnodes. This ensures modern Javascript engines can optimize virtual dom diffing by always compiling vnodes to the same hidden class.
|
||||
|
||||
When creating libraries that emit vnodes, you should use this module instead of writing naked Javascript objects in order to ensure a high level of rendering performance.
|
||||
Loading…
Add table
Add a link
Reference in a new issue