UNPKG

23.3 kBMarkdownView Raw
1# Snabbdom
2
3A virtual DOM library with focus on simplicity, modularity, powerful features
4and performance.
5
6[![Join the chat at https://gitter.im/paldepind/snabbdom](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/paldepind/snabbdom?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
8## Table of contents
9
10* [Introduction](#introduction)
11* [Features](#features)
12* [Inline example](#inline-example)
13* [Examples](#examples)
14* [Core documentation](#core-documentation)
15* [Modules documentation](#modules-documentation)
16* [Helpers](#helpers)
17* [Virtual Node documentation](#virtual-node)
18* [Structuring applications](#structuring-applications)
19
20## Why
21
22Virtual DOM is awesome. It allows us to express our application's view
23as a function of its state. But existing solutions were way way too
24bloated, too slow, lacked features, had an API biased towards OOP
25and/or lacked features I needed.
26
27## Introduction
28
29Snabbdom consists of an extremely simple, performant and extensible
30core that is only ≈ 200 SLOC. It offers a modular architecture with
31rich functionality for extensions through custom modules. To keep the
32core simple, all non-essential functionality is delegated to modules.
33
34You can mold Snabbdom into whatever you desire! Pick, choose and
35customize the functionality you want. Alternatively you can just use
36the default extensions and get a virtual DOM library with high
37performance, small size and all the features listed below.
38
39## Features
40
41* Core features
42 * About 200 SLOC – you could easily read through the entire core and fully
43 understand how it works.
44 * Extendable through modules.
45 * A rich set of hooks available, both per vnode and globally for modules,
46 to hook into any part of the diff and patch process.
47 * Splendid performance. Snabbdom is among the fastest virtual DOM libraries
48 in the [Virtual DOM Benchmark](http://vdom-benchmark.github.io/vdom-benchmark/).
49 * Patch function with a function signature equivalent to a reduce/scan
50 function. Allows for easier integration with a FRP library.
51* Features in modules
52 * `h` function for easily creating virtual DOM nodes.
53 * [SVG _just works_ with the `h` helper](#svg).
54 * Features for doing complex CSS animations.
55 * Powerful event listener functionality.
56 * [Thunks](#thunks) to optimize the diff and patch process even further.
57 * JSX support thanks to [snabbdom-jsx](https://github.com/yelouafi/snabbdom-jsx).
58 There is also a Babel plugin [babel-snabbdom-jsx](https://github.com/finnsson/babel-snabbdom-jsx).
59 * Server-side HTML output provided by
60 [snabbdom-to-html](https://github.com/acstll/snabbdom-to-html).
61 * Compact virtual DOM creation with [snabbdom-helpers](https://github.com/krainboltgreene/snabbdom-helpers).
62
63## Inline example
64
65```javascript
66var snabbdom = require('snabbdom');
67var patch = snabbdom.init([ // Init patch function with chosen modules
68 require('snabbdom/modules/class').default, // makes it easy to toggle classes
69 require('snabbdom/modules/props').default, // for setting properties on DOM elements
70 require('snabbdom/modules/style').default, // handles styling on elements with support for animations
71 require('snabbdom/modules/eventlisteners').default, // attaches event listeners
72]);
73var h = require('snabbdom/h').default; // helper function for creating vnodes
74
75var container = document.getElementById('container');
76
77var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
78 h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
79 ' and this is just normal text',
80 h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
81]);
82// Patch into empty DOM element – this modifies the DOM as a side effect
83patch(container, vnode);
84
85var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
86 h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
87 ' and this is still just normal text',
88 h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
89]);
90// Second `patch` invocation
91patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
92```
93
94## Examples
95
96* [Animated reordering of elements](http://snabbdom.github.io/snabbdom/examples/reorder-animation/)
97* [Hero transitions](http://snabbdom.github.io/snabbdom/examples/hero/)
98* [SVG Carousel](http://snabbdom.github.io/snabbdom/examples/carousel-svg/)
99
100## Core documentation
101
102The core of Snabbdom provides only the most essential functionality.
103It is designed to be as simple as possible while still being fast and
104extendable.
105
106### `snabbdom.init`
107
108The core exposes only one single function `snabbdom.init`. This `init`
109takes a list of modules and returns a `patch` function that uses the
110specified set of modules.
111
112```javascript
113var patch = snabbdom.init([
114 require('snabbdom/modules/class').default,
115 require('snabbdom/modules/style').default,
116]);
117```
118
119### `patch`
120
121The `patch` function returned by `init` takes two arguments. The first
122is a DOM element or a vnode representing the current view. The second
123is a vnode representing the new, updated view.
124
125If a DOM element with a parent is passed, `newVnode` will be turned
126into a DOM node, and the passed element will be replaced by the
127created DOM node. If an old vnode is passed, Snabbdom will efficiently
128modify it to match the description in the new vnode.
129
130Any old vnode passed must be the resulting vnode from a previous call
131to `patch`. This is necessary since Snabbdom stores information in the
132vnode. This makes it possible to implement a simpler and more
133performant architecture. This also avoids the creation of a new old
134vnode tree.
135
136```javascript
137patch(oldVnode, newVnode);
138```
139
140### `snabbdom/h`
141
142It is recommended that you use `snabbdom/h` to create vnodes. `h` accepts a
143tag/selector as a string, an optional data object and an optional string or
144array of children.
145
146```javascript
147var h = require('snabbdom/h').default;
148var vnode = h('div', {style: {color: '#000'}}, [
149 h('h1', 'Headline'),
150 h('p', 'A paragraph'),
151]);
152```
153
154### Hooks
155
156Hooks are a way to hook into the lifecycle of DOM nodes. Snabbdom
157offers a rich selection of hooks. Hooks are used both by modules to
158extend Snabbdom, and in normal code for executing arbitrary code at
159desired points in the life of a virtual node.
160
161#### Overview
162
163| Name | Triggered when | Arguments to callback |
164| ----------- | -------------- | ----------------------- |
165| `pre` | the patch process begins | none |
166| `init` | a vnode has been added | `vnode` |
167| `create` | a DOM element has been created based on a vnode | `emptyVnode, vnode` |
168| `insert` | an element has been inserted into the DOM | `vnode` |
169| `prepatch` | an element is about to be patched | `oldVnode, vnode` |
170| `update` | an element is being updated | `oldVnode, vnode` |
171| `postpatch` | an element has been patched | `oldVnode, vnode` |
172| `destroy` | an element is directly or indirectly being removed | `vnode` |
173| `remove` | an element is directly being removed from the DOM | `vnode, removeCallback` |
174| `post` | the patch process is done | none |
175
176The following hooks are available for modules: `pre`, `create`,
177`update`, `destroy`, `remove`, `post`.
178
179The following hooks are available in the `hook` property of individual
180elements: `init`, `create`, `insert`, `prepatch`, `update`,
181`postpatch`, `destroy`, `remove`.
182
183#### Usage
184
185To use hooks, pass them as an object to `hook` field of the data
186object argument.
187
188```javascript
189h('div.row', {
190 key: movie.rank,
191 hook: {
192 insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }
193 }
194});
195```
196
197#### The `init` hook
198
199This hook is invoked during the patch process when a new virtual node
200has been found. The hook is called before Snabbdom has processed the
201node in any way. I.e., before it has created a DOM node based on the
202vnode.
203
204#### The `insert` hook
205
206This hook is invoked once the DOM element for a vnode has been
207inserted into the document _and_ the rest of the patch cycle is done.
208This means that you can do DOM measurements (like using
209[getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
210in this hook safely, knowing that no elements will be changed
211afterwards that could affect the position of the inserted elements.
212
213#### The `remove` hook
214
215Allows you to hook into the removal of an element. The hook is called
216once a vnode is to be removed from the DOM. The handling function
217receives both the vnode and a callback. You can control and delay the
218removal with the callback. The callback should be invoked once the
219hook is done doing its business, and the element will only be removed
220once all `remove` hooks have invoked their callback.
221
222The hook is only triggered when an element is to be removed from its
223parent – not if it is the child of an element that is removed. For
224that, see the `destroy` hook.
225
226#### The `destroy` hook
227
228This hook is invoked on a virtual node when its DOM element is removed
229from the DOM or if its parent is being removed from the DOM.
230
231To see the difference between this hook and the `remove` hook,
232consider an example.
233
234```js
235var vnode1 = h('div', [h('div', [h('span', 'Hello')])]);
236var vnode2 = h('div', []);
237patch(container, vnode1);
238patch(vnode1, vnode2);
239```
240
241Here `destroy` is triggered for both the inner `div` element _and_ the
242`span` element it contains. `remove`, on the other hand, is only
243triggered on the `div` element because it is the only element being
244detached from its parent.
245
246You can, for instance, use `remove` to trigger an animation when an
247element is being removed and use the `destroy` hook to additionally
248animate the disappearance of the removed element's children.
249
250### Creating modules
251
252Modules works by registering global listeners for [hooks](#hooks). A module is simply a dictionary mapping hook names to functions.
253
254```javascript
255var myModule = {
256 create: function(oldVnode, vnode) {
257 // invoked whenever a new virtual node is created
258 },
259 update: function(oldVnode, vnode) {
260 // invoked whenever a virtual node is updated
261 }
262};
263```
264
265With this mechanism you can easily augment the behaviour of Snabbdom.
266For demonstration, take a look at the implementations of the default
267modules.
268
269## Modules documentation
270
271This describes the core modules. All modules are optional.
272
273### The class module
274
275The class module provides an easy way to dynamically toggle classes on
276elements. It expects an object in the `class` data property. The
277object should map class names to booleans that indicates whether or
278not the class should stay or go on the vnode.
279
280```javascript
281h('a', {class: {active: true, selected: false}}, 'Toggle');
282```
283
284### The props module
285
286Allows you to set properties on DOM elements.
287
288```javascript
289h('a', {props: {href: '/foo'}}, 'Go to Foo');
290```
291
292### The attributes module
293
294Same as props, but set attributes instead of properties on DOM elements.
295
296```javascript
297h('a', {attrs: {href: '/foo'}}, 'Go to Foo');
298```
299
300Attributes are added and updated using `setAttribute`. In case of an
301attribute that had been previously added/set and is no longer present
302in the `attrs` object, it is removed from the DOM element's attribute
303list using `removeAttribute`.
304
305In the case of boolean attributes (e.g. `disabled`, `hidden`,
306`selected` ...), the meaning doesn't depend on the attribute value
307(`true` or `false`) but depends instead on the presence/absence of the
308attribute itself in the DOM element. Those attributes are handled
309differently by the module: if a boolean attribute is set to a
310[falsy value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)
311(`0`, `-0`, `null`, `false`,`NaN`, `undefined`, or the empty string
312(`""`)), then the attribute will be removed from the attribute list of
313the DOM element.
314
315### The style module
316
317The style module is for making your HTML look slick and animate smoothly. At
318its core it allows you to set CSS properties on elements.
319
320```javascript
321h('span', {
322 style: {border: '1px solid #bada55', color: '#c0ffee', fontWeight: 'bold'}
323}, 'Say my name, and every colour illuminates');
324```
325
326Note that the style module does not remove style attributes if they
327are removed as properties from the style object. To remove a style,
328you should instead set it to the empty string.
329
330```javascript
331h('div', {
332 style: {position: shouldFollow ? 'fixed' : ''}
333}, 'I, I follow, I follow you');
334```
335
336#### Custom properties (CSS variables)
337
338CSS custom properties (aka CSS variables) are supported, they must be prefixed
339with `--`
340
341```javascript
342h('div', {
343 style: {'--warnColor': 'yellow'}
344}, 'Warning');
345```
346
347#### Delayed properties
348
349You can specify properties as being delayed. Whenever these properties
350change, the change is not applied until after the next frame.
351
352```javascript
353h('span', {
354 style: {opacity: '0', transition: 'opacity 1s', delayed: {opacity: '1'}}
355}, 'Imma fade right in!');
356```
357
358This makes it easy to declaratively animate the entry of elements.
359
360#### Set properties on `remove`
361
362Styles set in the `remove` property will take effect once the element
363is about to be removed from the DOM. The applied styles should be
364animated with CSS transitions. Only once all the styles are done
365animating will the element be removed from the DOM.
366
367```javascript
368h('span', {
369 style: {opacity: '1', transition: 'opacity 1s',
370 remove: {opacity: '0'}}
371}, 'It\'s better to fade out than to burn away');
372```
373
374This makes it easy to declaratively animate the removal of elements.
375
376#### Set properties on `destroy`
377
378```javascript
379h('span', {
380 style: {opacity: '1', transition: 'opacity 1s',
381 destroy: {opacity: '0'}}
382}, 'It\'s better to fade out than to burn away');
383```
384
385### Eventlisteners module
386
387The event listeners module gives powerful capabilities for attaching
388event listeners.
389
390You can attach a function to an event on a vnode by supplying an
391object at `on` with a property corresponding to the name of the event
392you want to listen to. The function will be called when the event
393happens and will be passed the event object that belongs to it.
394
395```javascript
396function clickHandler(ev) { console.log('got clicked'); }
397h('div', {on: {click: clickHandler}});
398```
399
400Very often, however, you're not really interested in the event object
401itself. Often you have some data associated with the element that
402triggers an event and you want that data passed along instead.
403
404Consider a counter application with three buttons, one to increment
405the counter by 1, one to increment the counter by 2 and one to
406increment the counter by 3. You don't really care exactly which button
407was pressed. Instead you're interested in what number was associated
408with the clicked button. The event listeners module allows one to
409express that by supplying an array at the named event property. The
410first element in the array should be a function that will be invoked
411with the value in the second element once the event occurs.
412
413```javascript
414function clickHandler(number) { console.log('button ' + number + ' was clicked!'); }
415h('div', [
416 h('a', {on: {click: [clickHandler, 1]}}),
417 h('a', {on: {click: [clickHandler, 2]}}),
418 h('a', {on: {click: [clickHandler, 3]}}),
419]);
420```
421
422Snabbdom allows swapping event handlers between renders. This happens without
423actually touching the event handlers attached to the DOM.
424
425Note, however, that **you should be careful when sharing event
426handlers between vnodes**, because of the technique this module uses
427to avoid re-binding event handlers to the DOM. (And in general,
428sharing data between vnodes is not guaranteed to work, because modules
429are allowed to mutate the given data).
430
431In particular, you should **not** do something like this:
432
433```javascript
434// Does not work
435var sharedHandler = {
436 change: function(e){ console.log('you chose: ' + e.target.value); }
437};
438h('div', [
439 h('input', {props: {type: 'radio', name: 'test', value: '0'},
440 on: sharedHandler}),
441 h('input', {props: {type: 'radio', name: 'test', value: '1'},
442 on: sharedHandler}),
443 h('input', {props: {type: 'radio', name: 'test', value: '2'},
444 on: sharedHandler})
445]);
446```
447
448For many such cases, you can use array-based handlers instead (described above).
449Alternatively, simply make sure each node is passed unique `on` values:
450
451```javascript
452// Works
453var sharedHandler = function(e){ console.log('you chose: ' + e.target.value); };
454h('div', [
455 h('input', {props: {type: 'radio', name: 'test', value: '0'},
456 on: {change: sharedHandler}}),
457 h('input', {props: {type: 'radio', name: 'test', value: '1'},
458 on: {change: sharedHandler}}),
459 h('input', {props: {type: 'radio', name: 'test', value: '2'},
460 on: {change: sharedHandler}})
461]);
462```
463
464## Helpers
465
466### SVG
467
468SVG just works when using the `h` function for creating virtual
469nodes. SVG elements are automatically created with the appropriate
470namespaces.
471
472```javascript
473var vnode = h('div', [
474 h('svg', {attrs: {width: 100, height: 100}}, [
475 h('circle', {attrs: {cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow'}})
476 ])
477]);
478```
479
480See also the [SVG example](./examples/svg) and the [SVG Carousel example](./examples/carousel-svg/).
481
482#### Using Classes
483Due to a bug in certain browsers like IE 11 and below and UC Browser, SVG Objects in these browsers do not support classlist property. Hence, the classes module (which uses classlist property internally) will not work for these browsers.
484
485Also, using snabbdom/h to create an element by passing a className along with the element type will not work as className property is read-only for SVG elements.
486
487You can add classes to SVG elements for both of these cases by using the attributes module as shown below:-
488```javascript
489h('text', {
490 attrs: {
491 x: xPos,
492 y: yPos,
493 dy: "5",
494 class: 'text_class'
495 }},
496 text
497);
498```
499
500### Thunks
501
502The `thunk` function takes a selector, a key for identifying a thunk,
503a function that returns a vnode and a variable amount of state
504parameters. If invoked, the render function will receive the state
505arguments.
506
507`thunk(selector, key, renderFn, [stateArguments])`
508
509The `key` is optional. It should be supplied when the `selector` is
510not unique among the thunks siblings. This ensures that the thunk is
511always matched correctly when diffing.
512
513Thunks are an optimization strategy that can be used when one is
514dealing with immutable data.
515
516Consider a simple function for creating a virtual node based on a number.
517
518```js
519function numberView(n) {
520 return h('div', 'Number is: ' + n);
521}
522```
523
524The view depends only on `n`. This means that if `n` is unchanged,
525then creating the virtual DOM node and patching it against the old
526vnode is wasteful. To avoid the overhead we can use the `thunk` helper
527function.
528
529```js
530function render(state) {
531 return thunk('num', numberView, [state.number]);
532}
533```
534
535Instead of actually invoking the `numberView` function this will only
536place a dummy vnode in the virtual tree. When Snabbdom patches this
537dummy vnode against a previous vnode, it will compare the value of
538`n`. If `n` is unchanged it will simply reuse the old vnode. This
539avoids recreating the number view and the diff process altogether.
540
541The view function here is only an example. In practice thunks are only
542relevant if you are rendering a complicated view that takes
543significant computational time to generate.
544
545## Virtual Node
546**Properties**
547 - [sel](#sel--string)
548 - [data](#data--object)
549 - [children](#children--array)
550 - [text](#text--string)
551 - [elm](#elm--element)
552 - [key](#key--string--number)
553
554#### sel : String
555
556The `.sel` property of a virtual node is the CSS selector passed to
557[`h()`](#snabbdomh) during creation. For example: `h('div#container',
558{}, [...])` will create a a virtual node which has `div#container` as
559its `.sel` property.
560
561#### data : Object
562
563The `.data` property of a virtual node is the place to add information
564for [modules](#modules-documentation) to access and manipulate the
565real DOM element when it is created; Add styles, CSS classes,
566attributes, etc.
567
568The data object is the (optional) second parameter to [`h()`](#snabbdomh)
569
570For example `h('div', {props: {className: 'container'}}, [...])` will produce a virtual node with
571```js
572{
573 "props": {
574 className: "container"
575 }
576}
577```
578as its `.data` object.
579
580#### children : Array<vnode>
581
582The `.children` property of a virtual node is the third (optional)
583parameter to [`h()`](#snabbdomh) during creation. `.children` is
584simply an Array of virtual nodes that should be added as children of
585the parent DOM node upon creation.
586
587For example `h('div', {}, [ h('h1', {}, 'Hello, World') ])` will
588create a virtual node with
589
590```js
591[
592 {
593 sel: 'h1',
594 data: {},
595 children: undefined,
596 text: 'Hello, World',
597 elm: Element,
598 key: undefined,
599 }
600]
601```
602
603as its `.children` property.
604
605#### text : string
606
607The `.text` property is created when a virtual node is created with
608only a single child that possesses text and only requires
609`document.createTextNode()` to be used.
610
611For example: `h('h1', {}, 'Hello')` will create a virtual node with
612`Hello` as its `.text` property.
613
614#### elm : Element
615
616The `.elm` property of a virtual node is a pointer to the real DOM
617node created by snabbdom. This property is very useful to do
618calculations in [hooks](#hooks) as well as
619[modules](#modules-documentation).
620
621#### key : string | number
622
623The `.key` property is created when a key is provided inside of your
624[`.data`](#data--object) object. The `.key` property is used to keep
625pointers to DOM nodes that existed previously to avoid recreating them
626if it is unnecessary. This is very useful for things like list
627reordering. A key must be either a string or a number to allow for
628proper lookup as it is stored internally as a key/value pair inside of
629an object, where `.key` is the key and the value is the
630[`.elm`](#elm--element) property created.
631
632For example: `h('div', {key: 1}, [])` will create a virtual node
633object with a `.key` property with the value of `1`.
634
635
636## Structuring applications
637
638Snabbdom is a low-level virtual DOM library. It is unopinionated with
639regards to how you should structure your application.
640
641Here are some approaches to building applications with Snabbdom.
642
643* [functional-frontend-architecture](https://github.com/paldepind/functional-frontend-architecture) –
644 a repository containing several example applications that
645 demonstrates an architecture that uses Snabbdom.
646* [Cycle.js](https://cycle.js.org/) –
647 "A functional and reactive JavaScript framework for cleaner code"
648 uses Snabbdom
649* [Vue.js](http://vuejs.org/) use a fork of snabbdom.
650* [scheme-todomvc](https://github.com/amirouche/scheme-todomvc/) build
651 redux-like architecture on top of snabbdom bindings.
652* [kaiju](https://github.com/AlexGalays/kaiju) -
653 Stateful components and observables on top of snabbdom
654
655Be sure to share it if you're building an application in another way
656using Snabbdom.
657