diff --git a/docs/components.md b/docs/components.md index d4016e03..db68927b 100644 --- a/docs/components.md +++ b/docs/components.md @@ -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"})) +``` \ No newline at end of file diff --git a/docs/lifecycle-methods.md b/docs/lifecycle-methods.md index ab702434..1ed2f956 100644 --- a/docs/lifecycle-methods.md +++ b/docs/lifecycle-methods.md @@ -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. diff --git a/docs/prop.md b/docs/prop.md index 1ad7fe75..30e0e325 100644 --- a/docs/prop.md +++ b/docs/prop.md @@ -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" -``` \ No newline at end of file +``` + +--- + +### 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) diff --git a/docs/render.md b/docs/render.md index 29a4993e..68f8e8a8 100644 --- a/docs/render.md +++ b/docs/render.md @@ -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) --- diff --git a/docs/request.md b/docs/request.md index dbe6c3d4..41837afe 100644 --- a/docs/request.md +++ b/docs/request.md @@ -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 }) ``` diff --git a/docs/trust.md b/docs/trust.md index 3aa99f98..d57815aa 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -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 = "" +var view = m("div", userContent) + +m.render(document.body, view) + +// equivalent HTML +//
<script>alert('evil')</script>
+``` + 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 `` or `