Docs - prioritize closure components for state (#2292)
* Emphasize closure components in components.md * Use closure components for all stateful component examples * Add change-log entry * Edits and separate sections for closure, class & POJO state
This commit is contained in:
parent
4ac33fa483
commit
a147023f4e
6 changed files with 213 additions and 101 deletions
|
|
@ -96,13 +96,17 @@ Mithril also does not redraw after lifecycle methods. Parts of the UI may be red
|
|||
If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw.
|
||||
|
||||
```javascript
|
||||
var StableComponent = {
|
||||
oncreate: function(vnode) {
|
||||
vnode.state.height = vnode.dom.offsetHeight
|
||||
m.redraw()
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "This component is " + vnode.state.height + "px tall")
|
||||
function StableComponent() {
|
||||
var height = 0
|
||||
|
||||
return {
|
||||
oncreate: function(vnode) {
|
||||
height = vnode.dom.offsetHeight
|
||||
m.redraw()
|
||||
},
|
||||
view: function() {
|
||||
return m("div", "This component is " + height + "px tall")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
|
||||
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
|
||||
- API: Introduction of `m.prop()` ([#2268](https://github.com/MithrilJS/mithril.js/pull/2268))
|
||||
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,56 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl
|
|||
|
||||
### Syntactic variants
|
||||
|
||||
#### Closure components
|
||||
|
||||
One of the easiest ways to manage state in a component is with a closure. A "closure component" is one that returns an object with a view function and optionally other lifecycle hooks. It has the ability to manage instance state within the body of the outer function.
|
||||
|
||||
```javascript
|
||||
function ClosureComponent(initialVnode) {
|
||||
// Each instance of this component has its own instance of `kind`
|
||||
var kind = "closure component"
|
||||
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("div", "Hello from a " + kind)
|
||||
},
|
||||
oncreate: function(vnode) {
|
||||
console.log("We've created a " + kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The returned object must hold a `view` function, used to get the tree to render.
|
||||
|
||||
They can be consumed in the same way regular components can.
|
||||
|
||||
```javascript
|
||||
// EXAMPLE: via m.render
|
||||
m.render(document.body, m(ClosureComponent))
|
||||
|
||||
// EXAMPLE: via m.mount
|
||||
m.mount(document.body, ClosureComponent)
|
||||
|
||||
// EXAMPLE: via m.route
|
||||
m.route(document.body, "/", {
|
||||
"/": ClosureComponent
|
||||
})
|
||||
|
||||
// EXAMPLE: component composition
|
||||
function AnotherClosureComponent() {
|
||||
return {
|
||||
view: function() {
|
||||
return m("main",
|
||||
m(ClosureComponent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If a component does *not* have state then you should opt for the simpler POJO component to avoid the additional overhead and boilerplate of the closure.
|
||||
|
||||
#### ES6 classes
|
||||
|
||||
Components can also be written using ES6 class syntax:
|
||||
|
|
@ -110,7 +160,6 @@ Components can also be written using ES6 class syntax:
|
|||
```javascript
|
||||
class ES6ClassComponent {
|
||||
constructor(vnode) {
|
||||
// vnode.state is undefined at this point
|
||||
this.kind = "ES6 class"
|
||||
}
|
||||
view() {
|
||||
|
|
@ -148,57 +197,9 @@ class AnotherES6ClassComponent {
|
|||
}
|
||||
```
|
||||
|
||||
#### Closure components
|
||||
|
||||
Functionally minded developers may prefer using the "closure component" syntax:
|
||||
|
||||
```javascript
|
||||
function closureComponent(vnode) {
|
||||
// vnode.state is undefined at this point
|
||||
var kind = "closure component"
|
||||
|
||||
return {
|
||||
view: function() {
|
||||
return m("div", "Hello from a " + kind)
|
||||
},
|
||||
oncreate: function() {
|
||||
console.log("We've created a " + kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The returned object must hold a `view` function, used to get the tree to render.
|
||||
|
||||
They can be consumed in the same way regular components can.
|
||||
|
||||
```javascript
|
||||
// EXAMPLE: via m.render
|
||||
m.render(document.body, m(closureComponent))
|
||||
|
||||
// EXAMPLE: via m.mount
|
||||
m.mount(document.body, closureComponent)
|
||||
|
||||
// EXAMPLE: via m.route
|
||||
m.route(document.body, "/", {
|
||||
"/": closureComponent
|
||||
})
|
||||
|
||||
// EXAMPLE: component composition
|
||||
function anotherClosureComponent() {
|
||||
return {
|
||||
view: function() {
|
||||
return m("main", [
|
||||
m(closureComponent)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mixing component kinds
|
||||
|
||||
Components can be freely mixed. A Class component can have closure or POJO components as children, etc...
|
||||
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -206,7 +207,100 @@ Components can be freely mixed. A Class component can have closure or POJO compo
|
|||
|
||||
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.
|
||||
Note that unlike many other frameworks, component state does *not* trigger [redraws](autoredraw.md) or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by [m.request](request.md) complete or when the browser navigates to different routes. Mithril's component state mechanisms simply exist as a convenience for applications.
|
||||
|
||||
If a state change occurs that is not as a result of any of the above conditions (e.g. after a `setTimeout`), then you can use `m.redraw()` to trigger a redraw manually.
|
||||
|
||||
#### Closure Component State
|
||||
|
||||
With a closure component state can simply be maintained by variables that are declared within the outer function. For example:
|
||||
|
||||
```javascript
|
||||
function ComponentWithState() {
|
||||
// Variables that hold component state
|
||||
var count = 0
|
||||
|
||||
return {
|
||||
view: function() {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: function() {
|
||||
count += 1
|
||||
}
|
||||
}, "Increment count")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Any functions declared within the closure also have access to its state variables.
|
||||
|
||||
```javascript
|
||||
function ComponentWithState() {
|
||||
var count = 0
|
||||
|
||||
function increment() {
|
||||
count += 1
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
count -= 1
|
||||
}
|
||||
|
||||
return {
|
||||
view: function() {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: increment
|
||||
}, "Increment"),
|
||||
m("button", {
|
||||
onclick: decrement
|
||||
}, "Decrement")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A big advantage of closure components is that we don't need to worry about binding `this` when attaching event handler callbacks. In fact `this` is never used at all and we never have to think about `this` context ambiguities.
|
||||
|
||||
#### Class Component State
|
||||
|
||||
With classes, state can be managed by class instance properties and methods. For example:
|
||||
|
||||
```javascript
|
||||
class ComponentWithState() {
|
||||
constructor() {
|
||||
this.count = 0
|
||||
}
|
||||
increment() {
|
||||
this.count += 1
|
||||
}
|
||||
decrement() {
|
||||
this.count -= 1
|
||||
}
|
||||
view() {
|
||||
return m("div",
|
||||
m("p", "Count: " + count),
|
||||
m("button", {
|
||||
onclick: () => {this.increment()}
|
||||
}, "Increment"),
|
||||
m("button", {
|
||||
onclick: () => {this.decrement()}
|
||||
}, "Decrement")
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
|
||||
|
||||
#### POJO Component State
|
||||
|
||||
For POJO components 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
|
||||
|
||||
|
|
@ -228,10 +322,6 @@ m(ComponentWithInitialState)
|
|||
// <div>Initial content</div>
|
||||
```
|
||||
|
||||
For class components, the state is an instance of the class, set right after the constructor is called.
|
||||
|
||||
For closure components, the state is the object returned by the closure, set right after the closure returns. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead).
|
||||
|
||||
#### 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.
|
||||
|
|
@ -351,9 +441,18 @@ var Auth = require("../models/Auth")
|
|||
var Login = {
|
||||
view: function() {
|
||||
return m(".login", [
|
||||
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
|
||||
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
|
||||
m("button", {disabled: !Auth.canSubmit(), onclick: Auth.login}, "Login"),
|
||||
m("input[type=text]", {
|
||||
oninput: m.withAttr("value", Auth.setUsername),
|
||||
value: Auth.username
|
||||
}),
|
||||
m("input[type=password]", {
|
||||
oninput: m.withAttr("value", Auth.setPassword),
|
||||
value: Auth.password
|
||||
}),
|
||||
m("button", {
|
||||
disabled: !Auth.canSubmit(),
|
||||
onclick: Auth.login
|
||||
}, "Login")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@ Instead, prefer using Javascript expressions such as the ternary operator and Ar
|
|||
```javascript
|
||||
// PREFER
|
||||
var BetterListComponent = {
|
||||
view: function() {
|
||||
view: function(vnode) {
|
||||
return m("ul", vnode.attrs.items.map(function(item) {
|
||||
return m("li", item)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -60,19 +60,24 @@ Like in other hooks, the `this` keyword in the `oninit` callback points to `vnod
|
|||
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) // displays data from initialization time
|
||||
function ComponentWithState() {
|
||||
var initialData
|
||||
return {
|
||||
oninit: function(vnode) {
|
||||
initialData = vnode.attrs.data
|
||||
},
|
||||
view: function(vnode) {
|
||||
return [
|
||||
// displays data from initialization time:
|
||||
m("div", "Initial: " + initialData),
|
||||
// displays current data:
|
||||
m("div", "Current: " + vnode.attrs.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.
|
||||
|
|
@ -110,17 +115,19 @@ The `onupdate(vnode)` hook is called after a DOM element is updated, while attac
|
|||
|
||||
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.
|
||||
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() {}
|
||||
function RedrawReporter() {
|
||||
var count = 0
|
||||
return {
|
||||
onupdate: function() {
|
||||
console.log("Redraws so far: ", ++count)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
}
|
||||
|
||||
m(RedrawReporter, {data: "Hello"})
|
||||
|
|
@ -163,16 +170,17 @@ Like in other hooks, the `this` keyword in the `onremove` callback points to `vn
|
|||
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() {}
|
||||
function Timer() {
|
||||
var timeout = setTimeout(function() {
|
||||
console.log("timed out")
|
||||
}, 1000)
|
||||
|
||||
return {
|
||||
onremove: function() {
|
||||
clearTimeout(timeout)
|
||||
},
|
||||
view: function() {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
18
docs/prop.md
18
docs/prop.md
|
|
@ -54,15 +54,15 @@ The `m.prop` method creates a prop, a getter/setter object wrapping a single mut
|
|||
In conjunction with [`m.withAttr`](withAttr.md), you can emulate two-way binding pretty easily.
|
||||
|
||||
```javascript
|
||||
var Component = {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.current = m.prop("")
|
||||
},
|
||||
view: function(vnode) {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", vnode.state.current.set),
|
||||
value: vnode.state.current.get(),
|
||||
})
|
||||
function Component() {
|
||||
var current = m.prop("")
|
||||
return {
|
||||
view: function(vnode) {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", current.set),
|
||||
value: current.get(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue