update docs re: component anti-patterns, lint docs

This commit is contained in:
Leo Horie 2016-07-07 13:23:16 -04:00
parent 34bed08cd0
commit 7318a0d88b
7 changed files with 224 additions and 13 deletions

View file

@ -179,7 +179,7 @@ var FlexibleComponent = {
#### Avoid magic indexes
Often it's desireable to define multiple sets of children, for example, if a component has a configurable title and body.
Often it's desirable 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.
@ -236,3 +236,32 @@ m(Header, {
tagline: m("h2", "Lorem ipsum"),
})
```
#### Avoid component factories
Component diffing relies on strict equality checking, so you should avoid recreating components. Instead, consume components idiomatically.
```javascript
// AVOID
var ComponentFactory = function(greeting) {
// creates a new component on every call
return {
view: function() {
return m("div", greeting)
}
}
}
m.render(document.body, m(ComponentFactory("hello")))
// caling a second time recreates div from scratch rather than doing nothing
m.render(document.body, m(ComponentFactory("hello")))
// PREFER
var Component = {
view: function(vnode) {
return m("div", vnode.attrs.greeting)
}
}
m.render(document.body, m(Component, {greeting: "hello"}))
// caling a second time does not modify DOM
m.render(document.body, m(Component, {greeting: "hello"}))
```

View file

@ -192,6 +192,64 @@ This hook is useful to reduce lag in updates in cases where there is a overly la
Although Mithril is flexible, some code patterns are discouraged:
#### Do not redraw synchronously from lifecycle hooks
The [`m.render`](render.md) method modifies DOM state, and therefore it's [non-reentrant](https://en.wikipedia.org/wiki/Reentrancy_(computing)). All lifecyle methods are called by `m.render()`, and therefore you cannot call `m.render`, `m.mount`, `m.route` or `m.redraw` from a lifecycle method. Redrawing synchronously from a lifecycle method will result in **undefined behavior**.
Typically, redrawing from an `oninit` or `onbeforeupdate` hook is meaningless since the element in question renders shortly after them anyways. If redrawing is required from any other hooks, you should consider moving code up the execution path; for example, refactor it so that the application code runs on an event handler, before its natural redraw occurs.
```javascript
// AVOID
var greeting = m.prop("")
var BrokenComponent = {
onupdate: function() {
this.greeting = greeting()
m.redraw()
},
view: function() {
return m("div[title=Hello]", {onclick: m.withAttr("title", greeting)}, this.greeting)
}
}
// PREFER
var greeting = m.prop("")
var WorkingComponent = {
view: function() {
return m("div[title=Hello]", {onclick: m.withAttr("title", greeting)}, greeting())
}
}
```
On rare occasions, there may not be a way to refactor a redraw out of a lifecycle method due to dependencies on layout values (e.g. scrollbar position, an element's updated offsetHeight, etc). In those cases, you should redraw asynchronously, by wrapping the redraw call in a `requestAnimationFrame`, `setTimeout` or similar function.
```javascript
// AVOID
var BrokenComponent = {
onupdate: function(vnode) {
var oldWidth = this.width
this.width = vnode.dom.offsetWidth
if (oldWidth !== this.width) m.redraw()
},
view: function() {
return m("div", {onclick: function() {console.log("calculating width")}}, "Width is: " + this.width)
}
}
// PREFER
var WorkingComponent = {
onupdate: function(vnode) {
var oldWidth = this.width
this.width = vnode.dom.offsetWidth
if (oldWidth !== this.width) requestAnimationFrame(m.redraw)
},
view: function() {
return m("div", {onclick: function() {console.log("calculating width")}}, "Width is: " + this.width)
}
}
```
#### Avoid premature optimizations
The `onbeforeupdate` hook should only be used as a last resort. Avoid using it unless you have a noticeable performance issue.

View file

@ -25,6 +25,7 @@
- [Stream states](#stream-states)
- [Handling errors](#handling-errors)
- [Serializing streams](#serializing-streams)
- [Streams do not trigger rendering](#streams-do-not-trigger-rendering)
---
@ -277,7 +278,7 @@ var RobustExample = {
}
m.route(document.body, "/", {
"/": MyComponent
"/": RobustExample
})
```
@ -289,6 +290,8 @@ When the request to the server completes, `req` is populated with the response d
If the request to the server fails, `catch` is called and `vnode.state.items()` is set to an empty array. Also, `req.error` is populated with the error, and `vnode.state.error` is populated with the vnode tree returned by `errorView`. Therefore, `view` returns `[[], m(".error", "An error occurred")]`, which replaces the loading icon with the error message in the DOM.
To clear the error message, simply set the value of the `vnode.state.error` stream to `undefined`.
---
### Streams vs promises
@ -395,7 +398,7 @@ var halted = m.prop(1).run(function(value) {
})
halted.run(function() {
//never runs
// never runs
})
```
@ -426,7 +429,7 @@ var halted = m.prop.combine(function(stream) {
}, [m.prop(1)])
halted.run(function() {
//never runs
// never runs
})
```
@ -655,11 +658,13 @@ console.log(recoveredStream()) // logs "hi"
console.log(recoveredStream.error()) // logs undefined
```
---
### Serializing streams
Streams implement a `.toJSON()` method. When a stream is passed as the argument to `JSON.stringify()`, the value of the stream is serialized.
```
```javascript
var stream = m.prop(123)
var serialized = JSON.stringify(stream)
console.log(serialized) // logs 123
@ -667,7 +672,15 @@ console.log(serialized) // logs 123
Streams also implement a `valueOf` method that returns the value of the stream.
```
```javascript
var stream = m.prop(123)
console.log("test " + stream) // logs "test 123"
```
```
---
### Streams do not trigger rendering
Unlike libraries like Knockout, Mithril streams do not trigger re-rendering of templates. Redrawing happens in response to event handlers defined in Mithril component views, route changes, or after [`m.request`](request.md) calls resolve.
If redrawing is desired in response to other asynchronous events (e.g. `setTimeout`/`setInterval`, websocket subscription, 3rd party library event handler, etc), you should manually call [`m.redraw()`](redraw.md)

View file

@ -23,7 +23,7 @@ Argument | Type | Required | Description
The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified where needed to reflect the changes.
This method is internally called by [`m.mount()`](mount.md), [`m.route()`](route.md) amd `[m.request()](request.md)`.
This method is internally called by [`m.mount()`](mount.md), [`m.route()`](route.md), [`m.redraw()`](redraw.md) and `[m.request()](request.md)`. It is not called by [`m.prop()`](prop.md)
---

View file

@ -79,7 +79,7 @@ m.route(document.body, "/", {
Let's assume making a request to the server URL `/api/items` returns an array of objects in JSON format.
When `m.route` is called at the bottom, `MyComponent` is initialized. `oninit` is called, which calls `m.request` and assigns its return value (a stream) to `vnode.state.items`. This stream contains the `initialValue` (i.e. an empty array), and this value can be retrieved by calling the stream as a function (i.e. `value = vnode.state.items()`). After the oninit method returns, the component is then rendered. Since `vnode.state.items()` returns an empty array, the component's `view` method also returns an empty array, so no DOM elements are created. When the request to the server completes, `m.request` parses the response data into a Javascript array of objects and sets the value of the stream to that array. Then, the component is rendered again. This time, `vnode.state.items()` returns a non-empty array, so the component's `view` method returns an array of vnodes, which in turn are rendered into `div` DOM elements.
When `m.route` is called at the bottom, `SimpleExample` is initialized. `oninit` is called, which calls `m.request` and assigns its return value (a stream) to `vnode.state.items`. This stream contains the `initialValue` (i.e. an empty array), and this value can be retrieved by calling the stream as a function (i.e. `value = vnode.state.items()`). After the oninit method returns, the component is then rendered. Since `vnode.state.items()` returns an empty array, the component's `view` method also returns an empty array, so no DOM elements are created. When the request to the server completes, `m.request` parses the response data into a Javascript array of objects and sets the value of the stream to that array. Then, the component is rendered again. This time, `vnode.state.items()` returns a non-empty array, so the component's `view` method returns an array of vnodes, which in turn are rendered into `div` DOM elements.
#### Loading icons and error messages
@ -111,7 +111,7 @@ var RobustExample = {
}
m.route(document.body, "/", {
"/": MyComponent
"/": RobustExample
})
```

View file

@ -27,6 +27,16 @@ Argument | Type | Required | Description
By default, Mithril escapes all values in order to prevent a class of security problems called [XSS injections](https://en.wikipedia.org/wiki/Cross-site_scripting).
```javascript
var userContent = "<script>alert('evil')</script>"
var view = m("div", userContent)
m.render(document.body, view)
// equivalent HTML
// <div>&lt;script&gt;alert('evil')&lt;/script&gt;</div>
```
However, sometimes it is desirable to render rich text and formatting markup. To fill that need, `m.trust` creates trusted HTML [vnodes](vnodes.md) which are rendered as HTML.
```javascript
@ -46,7 +56,7 @@ Trusted HTML vnodes are objects, not strings; therefore they cannot be concatena
### Security considerations
You **must sanitize the input** of `m.trust` to ensure there's no user-generated Javascript in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous javascript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.
You **must sanitize the input** of `m.trust` to ensure there's no user-generated malicious code in the HTML string. If you don't sanitize an HTML string and mark it as a trusted string, any asynchronous javascript call points within the HTML string will be triggered and run with the authorization level of the user viewing the page.
There are many ways in which an HTML string may contain executable code. The most common ways to inject security attacks are to add an `onload` or `onerror` attributes in `<img>` or `<iframe>` tags, and to use unbalanced quotes such as `" onerror="alert(1)` to inject executable contexts in unsanitized string interpolations.
@ -67,9 +77,12 @@ data.title = "' onerror='alert(1)"
// An attack using a different attribute
data.title = "' onmouseover='alert(1)"
// An attack that does not use javascript
data.description = "<a href='http://evil.com/login-page-that-steals-passwords.html'>Click here to read more</a>"
```
There are several non-obvious ways of declaring executable code, so it is highly recommended that you use a [whitelist](https://en.wikipedia.org/wiki/Whitelist) of permitted HTML tags, attributes and attribute values, as opposed to a [blacklist](https://en.wikipedia.org/wiki/Blacklisting) to sanitize the user input. It's also highly recommended that you use a proper HTML parser, instead of regular expressions for sanitization, because regular expressions are extremely difficult to test for all edge cases.
There are countless non-obvious ways of creating malicious code, so it is highly recommended that you use a [whitelist](https://en.wikipedia.org/wiki/Whitelist) of permitted HTML tags, attributes and attribute values, as opposed to a [blacklist](https://en.wikipedia.org/wiki/Blacklisting) to sanitize the user input. It's also highly recommended that you use a proper HTML parser, instead of regular expressions for sanitization, because regular expressions are extremely difficult to test for all edge cases.
---
@ -79,7 +92,7 @@ Even though there are many obscure ways to make an HTML string run Javascript, `
For historical reasons, browsers ignore `<script>` tags that are inserted into the DOM via innerHTML. They do this because once the element is ready (and thus, has an accessible innerHTML property), the rendering engines cannot backtrack to the parsing-stage if the script calls something like document.write("</body>").
This browser behavior may seem surprising to a developer coming from jQuery, because jQuery implements code specifically to find script tags and run them in this scenario. Mithril follows the browser behavior. If jQuery behavior is desired, you should consider moving the code out of the HTML string and into an `oncreate` [lifecycle method](lifecycle-methods.md), and either use jQuery or re-implement its script parsing routine.
This browser behavior may seem surprising to a developer coming from jQuery, because jQuery implements code specifically to find script tags and run them in this scenario. Mithril follows the browser behavior. If jQuery behavior is desired, you should consider either moving the code out of the HTML string and into an `oncreate` [lifecycle method](lifecycle-methods.md), or use jQuery (or re-implement its script parsing code).
---

98
docs/withAttr.md Normal file
View file

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