diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 00000000..b005149e --- /dev/null +++ b/docs/components.md @@ -0,0 +1,238 @@ +# Components + +- [Structure](#structure) +- [Lifecycle methods](#lifecycle-methods) +- [State](#state) +- [Avoid-anti-patterns](#avoid-anti-patterns) + +### 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`](hyperscript.md) utility: + +```javascript +var Example = { + view: function() { + return m("div", "Hello") + } +} + +m(Example) + +// equivalent HTML +//
Hello
+``` + +--- + +### Lifecycle methods + +Components can have the same [lifecycle methods](lifecycle-methods.md) as virtual DOM nodes: `oninit`, `oncreate`, `onupdate`, `onbeforeremove`, `onremove` and `shouldUpdate`. + +```javascript +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") + }, + shouldUpdate: 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. + +```javascript +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. + +To learn more about lifecycle methods, [see the lifecycle methods page](lifecycle-methods.md). + +--- + +### 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 `Input` component's state object. + +```javascript +var ComponentWithInitialState = { + data: "Initial content", + view: function(vnode) { + return m("div", vnode.state.data) + } +} + +m(ComponentWithInitialState) + +// Equivalent HTML +//
Initial content
+``` + +#### 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 +//
Hello
+``` + +#### 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() { + this.data = vnode.attrs.text + }, + view: function(vnode) { + return m("div", this.data) + } +} + +m(ComponentUsingThis, {text: "Hello"}) + +// Equivalent HTML +//
Hello
+``` + +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, of if ES6 is not available, use `vnode.state`. + +--- + +### Avoid anti-patterns + +Although Mithril is flexible, some code patterns are discouraged: + +#### Avoid restrictive interfaces + +A component has a restrictive interface when it exposes only specific properties, under the assumption that other properties will not be needed, or that they can be added at a later time. + +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). + +```javascript +// AVOID +var RestrictiveComponent = { + view: function(vnode) { + return m("button", {onclick: vnode.attrs.onclick}, [ + "Click to " + vnode.attrs.text + ]) + } +} +``` + +It's preferable to allow passing through parameters to a component's root node, if it makes sense to do so: + +```javascript +// PREFER +var FlexibleComponent = { + view: function(vnode) { + return m("button", vnode.attrs, [ + "Click to ", vnode.children + ]) + } +} +``` + +#### Avoid magic indexes + +Often it's desireable 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. + +```javascript +// 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 makes different children look different based on where they appear in the array. It's difficult to understand the component without reading its implementation. Instead, use attributes as named parameters and reserve `children` for uniform child content: + +```javascript +// 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(Header, { + title: [ + m("h1", "My title"), + m("small", "A small note"), + ], + tagline: m("h2", "Lorem ipsum"), +}) +``` diff --git a/docs/hyperscript.md b/docs/hyperscript.md new file mode 100644 index 00000000..7556c353 --- /dev/null +++ b/docs/hyperscript.md @@ -0,0 +1,412 @@ +# m(selector, attributes, children) + +- [Signature](#signature) +- [How it works](#how-it-works) +- [Flexibility](#flexibility) +- [CSS selectors](#css-selectors) +- [DOM attributes](#dom-attributes) +- [Style attribute](#style-attribute) +- [Events](#events) +- [Properties](#properties) +- [Components](#components) +- [Lifecycle methods](#lifecycle-methods) +- [Keys](#keys) +- [SVG and MathML](#svg-and-mathml) +- [Making templates dynamic](#making-templates-dynamic) +- [Avoid-anti-patterns](#avoid-anti-patterns) + +--- + +### Signature + +`vnode = m(selector, attributes, children)` + +Argument | Type | Required | Description +------------ | ------------------------------------------ | -------- | --- +`selector` | `String|Object` | Yes | A CSS selector or a component +`attributes` | `Object` | No | HTML attributes or element properties +`children` | `Array|String|Number|Boolean` | No | Child [vnodes](vnodes.md#structure). Can be written as [splat arguments](signatures.md#splats) +**returns** | `Vnode` | | A [vnode](vnodes.md#structure) + +[How to read signatures](signatures.md) + +--- + +### How it works + +Mithril provides a hyperscript function `m`, which allows expressing any HTML structure using javascript syntax. It accepts a `selector` string (required), an `attributes` object (optional) and a `children` array (optional). + +```javascript +var m = require("mithril") + +m("div", {id: "box"}, "hello") + +// equivalent HTML: +//
hello
+``` + +The `m` function does not actually return a DOM element. Instead it returns a [virtual DOM node](vnodes.md), or *vnode*, which is a javascript object that represents the DOM element to be created. + +```javascript +//a vnode +{tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]} +``` + +To transform a vnode into an actual DOM element, use the [`m.render()`](render.md) function: + +``` +m.render(document.body, m("br")) // puts a
in +``` + +Calling `m.render()` multiple times does **not** recreate the DOM tree from scratch each time. Instead, each call will only make a change to a DOM tree if it is absolutely necessary to reflect the virtual DOM tree passed into the call. This behavior is desirable because recreating the DOM from scratch is very expensive, and causes issues such as loss of input focus, among other things. By contrast, updating the DOM only where necessary is comparatively much faster and makes it easier to maintain complex UIs that handle multiple user stories. + +--- + +### Flexibility + +The `m` function is both *polymorphic* and *variadic*. In other words, it's very flexible in what it expects as input parameters: + +```javascript +//simple tag +m("div") //
+ +//attributes and children are optional +m("a", {id: "b"}) // +m("span", "hello") // hello + +//tag with child nodes +m("ul", [ // + +// array is optional +m("ul", // +``` + +--- + +### CSS selectors + +The first argument of `m` can be any CSS selector that can describe an HTML element. It accepts any valid CSS combinations of `#` (id), `.` (class) and `[]` (attribute) syntax. + +```javascript +m("div#hello") +//
+ +m("section.container") +//
+ +m("input[type=text][placeholder=Name]") +// + +m("a#exit.external[href='http://example.com']", "Leave") +// Leave +``` + +If you omit the tag name, Mithril assumes a `div` tag. + +```javascript +m(".box.box-bordered") //
+``` + +Typically, it's recommended that you use CSS selectors for static attributes (i.e. attributes whose value do not change), and pass an attributes object for dynamic attribute values. + +```javascript +var currentURL = "/" + +m("a.link[href=/]", { + class: currentURL === "/" ? "selected" : "" +}, "Home") + +//equivalent HTML: +Home +``` + +If there are class names in both first and second arguments of `m`, they are merged together as you would expect. + +--- + +### DOM attributes + +Mithril uses both the Javascript API and the DOM API (`setAttribute`) to resolve attributes. This means you can use both syntaxes to refer to attributes. + +For example, in the Javascript API, the `readonly` attribute is called `element.readOnly` (notice the uppercase). In Mithril, all of the following are supported: + +```javascript +m("input", {readonly: true}) //lowercase +m("input", {readOnly: true}) //uppercase +m("input[readonly]") +m("input[readOnly]") +``` + +--- + +### Style attribute + +Mithril supports both strings and objects as valid `style` values. In other words, all of the following are supported: + +```javascript +m("div", {style: "background:red;"}) +m("div", {style: {background: "red"}}) +m("div[style=background:red") +``` + +Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed. + +Mithril does not attempt to add units to number values. + +--- + +### Events + +Mithril supports event handler binding for all DOM events, including events whose specs do not define an `on` property, such as `touchstart` + +``` +function doSomething(e) { + console.log(e) +} + +m("div", {onclick: doSomething}) +``` + +--- + +### Properties + +Mithril supports DOM functionality that is accessible via properties such as `