diff --git a/docs/autoredraw.md b/docs/autoredraw.md index eb1f093f..6fcc48de 100644 --- a/docs/autoredraw.md +++ b/docs/autoredraw.md @@ -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") + } } } ``` diff --git a/docs/change-log.md b/docs/change-log.md index 65102983..b2893112 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -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 diff --git a/docs/components.md b/docs/components.md index 89ccf2b0..3bc1bc97 100644 --- a/docs/components.md +++ b/docs/components.md @@ -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) //