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 +//