Component docs update: an effective compromise re: state & syntactic variance (#2294)
* Update components.md * Update components.md * Update components.md * Update components.md * Update docs/components.md Co-Authored-By: CreaturesInUnitards <portrayme@mac.com> * Update docs/components.md Co-Authored-By: CreaturesInUnitards <portrayme@mac.com> * Update docs/components.md Co-Authored-By: CreaturesInUnitards <portrayme@mac.com> * teeny clarification * reasonably compromised language, I think
This commit is contained in:
parent
749f54d7cf
commit
52ccea2cad
1 changed files with 166 additions and 197 deletions
|
|
@ -2,23 +2,29 @@
|
||||||
|
|
||||||
- [Structure](#structure)
|
- [Structure](#structure)
|
||||||
- [Lifecycle methods](#lifecycle-methods)
|
- [Lifecycle methods](#lifecycle-methods)
|
||||||
- [Syntactic variants](#syntactic-variants)
|
- [Passing data to components](#passing-data-to-components)
|
||||||
- [State](#state)
|
- [State](#state)
|
||||||
|
- [Closure component state](#closure-component-state)
|
||||||
|
- [POJO component state](#pojo-component-state)
|
||||||
|
- [ES6 Classes](#es6-classes)
|
||||||
|
- [Class component state](#class-component-state)
|
||||||
- [Avoid anti-patterns](#avoid-anti-patterns)
|
- [Avoid anti-patterns](#avoid-anti-patterns)
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse.
|
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:
|
Any Javascript object that has a `view` method is a Mithril component. Components can be consumed via the [`m()`](hyperscript.md) utility:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// define your component
|
||||||
var Example = {
|
var Example = {
|
||||||
view: function() {
|
view: function(vnode) {
|
||||||
return m("div", "Hello")
|
return m("div", "Hello")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// consume your component
|
||||||
m(Example)
|
m(Example)
|
||||||
|
|
||||||
// equivalent HTML
|
// equivalent HTML
|
||||||
|
|
@ -27,31 +33,9 @@ m(Example)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Passing data to components
|
|
||||||
|
|
||||||
Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
m(Example, {name: "Floyd"})
|
|
||||||
```
|
|
||||||
|
|
||||||
This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var Example = {
|
|
||||||
view: function (vnode) {
|
|
||||||
return m("div", "Hello, " + vnode.attrs.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: Lifecycle methods can also be provided via the `attrs` object, so you should avoid using the lifecycle method names for your own callbacks as they would also be invoked by Mithril. Use lifecycle methods in `attrs` only when you specifically wish to create lifecycle hooks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Lifecycle methods
|
### Lifecycle methods
|
||||||
|
|
||||||
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove` and `onbeforeupdate`.
|
Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes. Note that `vnode` is passed as an argument to each lifecycle method, as well as to `view` (with the _previous_ vnode passed additionally to `onbeforeupdate`):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ComponentWithHooks = {
|
var ComponentWithHooks = {
|
||||||
|
|
@ -61,7 +45,7 @@ var ComponentWithHooks = {
|
||||||
oncreate: function(vnode) {
|
oncreate: function(vnode) {
|
||||||
console.log("DOM created")
|
console.log("DOM created")
|
||||||
},
|
},
|
||||||
onbeforeupdate: function(vnode, old) {
|
onbeforeupdate: function(newVnode, oldVnode) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
onupdate: function(vnode) {
|
onupdate: function(vnode) {
|
||||||
|
|
@ -86,7 +70,7 @@ var ComponentWithHooks = {
|
||||||
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
|
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function initialize() {
|
function initialize(vnode) {
|
||||||
console.log("initialized as vnode")
|
console.log("initialized as vnode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,61 +85,175 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Syntactic variants
|
### Passing data to components
|
||||||
|
|
||||||
#### Closure components
|
Data can be passed to component instances by passing an `attrs` object as the second parameter in the hyperscript function:
|
||||||
|
|
||||||
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
|
```javascript
|
||||||
function ClosureComponent(initialVnode) {
|
m(Example, {name: "Floyd"})
|
||||||
// Each instance of this component has its own instance of `kind`
|
```
|
||||||
var kind = "closure component"
|
|
||||||
|
|
||||||
return {
|
This data can be accessed in the component's view or lifecycle methods via the `vnode.attrs`:
|
||||||
view: function(vnode) {
|
|
||||||
return m("div", "Hello from a " + kind)
|
```javascript
|
||||||
},
|
var Example = {
|
||||||
oncreate: function(vnode) {
|
view: function (vnode) {
|
||||||
console.log("We've created a " + kind)
|
return m("div", "Hello, " + vnode.attrs.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The returned object must hold a `view` function, used to get the tree to render.
|
NOTE: Lifecycle methods can also be defined in the `attrs` object, so you should avoid using their names for your own callbacks as they would also be invoked by Mithril itself. Use them in `attrs` only when you specifically wish to use them as lifecycle methods.
|
||||||
|
|
||||||
They can be consumed in the same way regular components can.
|
---
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Note that unlike many other frameworks, mutating 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
|
||||||
|
|
||||||
|
In the above examples, each component is defined as a POJO (Plain Old Javascript Object), which is used by Mithril internally as the prototype for that component's instances. It's possible to use component state with a POJO (as we'll discuss below), but it's not the cleanest or simplest approach. For that we'll use a **_closure component_**, which is simply a wrapper function which _returns_ a POJO component instance, which in turn carries its own, closed-over scope.
|
||||||
|
|
||||||
|
With a closure component, state can simply be maintained by variables that are declared within the outer function:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// EXAMPLE: via m.render
|
function ComponentWithState(initialVnode) {
|
||||||
m.render(document.body, m(ClosureComponent))
|
// Component state variable, unique to each instance
|
||||||
|
var count = 0
|
||||||
|
|
||||||
// EXAMPLE: via m.mount
|
// POJO component instance: any object with a
|
||||||
m.mount(document.body, ClosureComponent)
|
// view function which returns a vnode
|
||||||
|
|
||||||
// EXAMPLE: via m.route
|
|
||||||
m.route(document.body, "/", {
|
|
||||||
"/": ClosureComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
// EXAMPLE: component composition
|
|
||||||
function AnotherClosureComponent() {
|
|
||||||
return {
|
return {
|
||||||
view: function() {
|
oninit: function(vnode){
|
||||||
return m("main",
|
console.log("init a closure component")
|
||||||
m(ClosureComponent)
|
},
|
||||||
|
view: function(vnode) {
|
||||||
|
return m("div",
|
||||||
|
m("p", "Count: " + count),
|
||||||
|
m("button", {
|
||||||
|
onclick: function() {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}, "Increment count")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Any functions declared within the closure also have access to its state variables.
|
||||||
|
|
||||||
#### ES6 classes
|
```javascript
|
||||||
|
function ComponentWithState(initialVnode) {
|
||||||
|
var count = 0
|
||||||
|
|
||||||
Components can also be written using ES6 class syntax:
|
function increment() {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement() {
|
||||||
|
count -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: function(vnode) {
|
||||||
|
return m("div",
|
||||||
|
m("p", "Count: " + count),
|
||||||
|
m("button", {
|
||||||
|
onclick: increment
|
||||||
|
}, "Increment"),
|
||||||
|
m("button", {
|
||||||
|
onclick: decrement
|
||||||
|
}, "Decrement")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Closure components are consumed in the same way as POJOs, e.g. `m(ComponentWithState, { passedData: ... })`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POJO Component State
|
||||||
|
|
||||||
|
It is generally recommended that you use closures for managing component state. If, however, you have reason to manage state in a POJO, the state of a component can be accessed in three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
|
||||||
|
|
||||||
|
#### At initialization
|
||||||
|
|
||||||
|
For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple "blueprint" state initialization.
|
||||||
|
|
||||||
|
In the example below, `data` becomes a property of the `ComponentWithInitialState` component's `vnode.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
|
||||||
|
|
||||||
|
As you can see, 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(vnode) {
|
||||||
|
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, or if ES6 is not available, use `vnode.state`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ES6 classes
|
||||||
|
|
||||||
|
If it suits your needs (like in object-oriented projects), components can also be written using ES6 class syntax:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class ES6ClassComponent {
|
class ES6ClassComponent {
|
||||||
|
|
@ -197,83 +295,13 @@ class AnotherES6ClassComponent {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mixing component kinds
|
|
||||||
|
|
||||||
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
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
|
#### Class Component State
|
||||||
|
|
||||||
With classes, state can be managed by class instance properties and methods. For example:
|
With classes, state can be managed by class instance properties and methods, and accessed via `this`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class ComponentWithState() {
|
class ComponentWithState {
|
||||||
constructor() {
|
constructor(vnode) {
|
||||||
this.count = 0
|
this.count = 0
|
||||||
}
|
}
|
||||||
increment() {
|
increment() {
|
||||||
|
|
@ -298,71 +326,12 @@ class ComponentWithState() {
|
||||||
|
|
||||||
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
|
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.
|
### Mixing component kinds
|
||||||
|
|
||||||
#### At initialization
|
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
|
||||||
|
|
||||||
For POJO components, the component object is the prototype of each component instance, so any property defined on the component object will be accessible as a property of `vnode.state`. This allows simple state initialization.
|
|
||||||
|
|
||||||
In the example below, `data` is a property of the `ComponentWithInitialState` 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(vnode) {
|
|
||||||
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, or if ES6 is not available, use `vnode.state`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue