8.2 KiB
Components
Structure
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() utility:
var Example = {
view: function() {
return m("div", "Hello")
}
}
m(Example)
// equivalent HTML
// <div>Hello</div>
Passing data to components
Data can be passed to component instances through an attrs object as a parameter in the hyperscript function:
m(Example, {name: "Floyd"})
attrs data can be accessed in the component's view or lifecycle methods via the vnode:
var Example = {
view: function (vnode) {
return m("div", "Hello, " + vnode.attrs.name)
}
}
NOTE: Lifecycle methods can also be provided via attrs, so you should avoid using the lifecycle method names for your own callbacks as they will be invoked by Mithril. Use lifecycle methods in attrs only when you specifically wish to create lifecycle hooks.
Lifecycle methods
Components can have the same lifecycle methods as virtual DOM nodes: oninit, oncreate, onupdate, onbeforeremove, onremove and onbeforeupdate.
var ComponentWithHooks = {
oninit: function(vnode) {
console.log("initialized")
},
oncreate: function(vnode) {
console.log("DOM created")
},
onupdate: function(vnode) {
console.log("DOM updated")
},
onbeforeremove: function(vnode, done) {
console.log("exit animation can start")
done()
},
onremove: function(vnode) {
console.log("removing DOM element")
},
onbeforeupdate: function(vnode, old) {
return true
},
view: function(vnode) {
return "hello"
}
}
Like other types of virtual DOM nodes, components may have additional lifecycle methods defined when consumed as vnode types.
function initialize() {
console.log("initialized as vnode")
}
m(ComponentWithHooks, {oninit: initialize})
Lifecycle methods in vnodes do not override component methods, nor vice versa. Component lifecycle methods are always run after the vnode's corresponding method.
Take care not to use lifecycle method names for your own callback function names in vnodes.
To learn more about lifecycle methods, see the lifecycle methods page.
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.
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
Any property attached to the component object is deep-cloned for every instance of the component. This allows simple state initialization.
In the example below, data is a property of the ComponentWithInitialState component's state object.
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.
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.
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.
Avoid anti-patterns
Try to keep component interfaces generic by default - use custom interfaces when implementation isn't generic.
Avoiding restrictive interfaces
A component has a restrictive interface when it is unnecessarily specific in its use of attributes. Custom attributes should be used when a component has a very specific purpose or requires special internal logic, but this often isn't the case - attributes and children should be used where possible.
In the example below, the button configuration is severely limited: it does not support any events other than onclick, it's not styleable and it only accepts text as children (but not elements, fragments or trusted HTML).
// AVOID
var RestrictiveComponent = {
view: function(vnode) {
return m("button", {onclick: vnode.attrs.onclick}, [
"Click to " + vnode.attrs.text
])
}
}
If the required attributes are equivalent to generic DOM attributes, it's preferable to allow passing through parameters to a component's root node, if it makes sense to do so.
// PREFER
var FlexibleComponent = {
view: function(vnode) {
return m("button", vnode.attrs, [
"Click to ", vnode.children
])
}
}
Don't manipulate children
However, if a component is opinionated in how it applies attributes or children, you should switch to using custom attributes.
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.
// AVOID
var Header = {
view: function(vnode) {
return m(".section", [
m(".header", vnode.children[0]),
m(".tagline", vnode.children[1]),
])
}
}
m(Header, [
m("h1", "My title"),
m("h2", "Lorem ipsum"),
])
// awkward consumption use case
m(Header, [
[
m("h1", "My title"),
m("small", "A small note"),
],
m("h2", "Lorem ipsum"),
])
The component above breaks the assumption that children will be output in the same contiguous format as they are received. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve children for uniform child content:
// PREFER
var BetterHeader = {
view: function(vnode) {
return m(".section", [
m(".header", vnode.attrs.title),
m(".tagline", vnode.attrs.tagline),
])
}
}
m(BetterHeader, {
title: m("h1", "My title"),
tagline: m("h2", "Lorem ipsum"),
})
// clearer consumption use case
m(BetterHeader, {
title: [
m("h1", "My title"),
m("small", "A small note"),
],
tagline: m("h2", "Lorem ipsum"),
})
Avoid component factories
If you create a component from within a view method (either directly inline or by calling a function that does so), each redraw will have a different clone of the component. When diffing component vnodes, if the component referenced by the new vnode is not strictly equal to the one referenced by the old component, the two are assumed to be different components even if they ultimately run equivalent code. This means components created dynamically via a factory will always be re-created from scratch.
For that reason you should avoid recreating components. Instead, consume components idiomatically.
// 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")))
// calling 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"}))
// calling a second time does not modify DOM
m.render(document.body, m(Component, {greeting: "hello"}))