@thi.ng/hdom
Advanced tools
Comparing version 2.3.3 to 3.0.0
import { IObjectOf } from "@thi.ng/api/api"; | ||
export interface ILifecycle { | ||
init?(el: Element, ...args: any[]): any; | ||
render(...args: any[]): any; | ||
release?(...args: any[]): any; | ||
init?(el: Element, ctx: any, ...args: any[]): any; | ||
render(ctx: any, ...args: any[]): any; | ||
release?(ctx: any, ...args: any[]): any; | ||
} | ||
@@ -7,0 +7,0 @@ export interface ComponentAttribs { |
@@ -6,2 +6,20 @@ # Change Log | ||
<a name="3.0.0"></a> | ||
# [3.0.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@2.3.3...@thi.ng/hdom@3.0.0) (2018-04-08) | ||
### Features | ||
* **hdom:** fix [#13](https://github.com/thi-ng/umbrella/issues/13), add support for user context and pass to components ([70cfe06](https://github.com/thi-ng/umbrella/commit/70cfe06)) | ||
### BREAKING CHANGES | ||
* **hdom:** component functions & lifecycle hooks now receive user | ||
context object as their first arg. All components accepting arguments must | ||
be updated, but can potentially be simplified at the same time. | ||
<a name="2.3.3"></a> | ||
@@ -8,0 +26,0 @@ ## [2.3.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@2.3.2...@thi.ng/hdom@2.3.3) (2018-04-04) |
@@ -1,1 +0,20 @@ | ||
export declare function diffElement(parent: Element, prev: any, curr: any): void; | ||
/** | ||
* Takes a DOM root element and two hiccup trees, `prev` and `curr`. | ||
* Recursively computes diff between both trees and applies any | ||
* necessary changes to reflect `curr` tree in real DOM. | ||
* | ||
* For newly added components, calls `init` with created DOM element | ||
* (plus user provided context and any other args) for any components | ||
* with `init` life cycle method. Likewise, calls `release` on | ||
* components with `release` method when the DOM element is removed. | ||
* | ||
* Important: The actual DOM element given is assumed to exactly | ||
* represent the state of the `prev` tree. Since this function does NOT | ||
* track the real DOM at all, the resulting changes will result in | ||
* potentially undefined behavior if there're discrepancies. | ||
* | ||
* @param root | ||
* @param prev previous tree | ||
* @param curr current tree | ||
*/ | ||
export declare function diffElement(root: Element, prev: any, curr: any): void; |
54
diff.js
@@ -10,4 +10,23 @@ "use strict"; | ||
const diffObject = diff.diffObject; | ||
function diffElement(parent, prev, curr) { | ||
_diffElement(parent, prev, curr, 0); | ||
/** | ||
* Takes a DOM root element and two hiccup trees, `prev` and `curr`. | ||
* Recursively computes diff between both trees and applies any | ||
* necessary changes to reflect `curr` tree in real DOM. | ||
* | ||
* For newly added components, calls `init` with created DOM element | ||
* (plus user provided context and any other args) for any components | ||
* with `init` life cycle method. Likewise, calls `release` on | ||
* components with `release` method when the DOM element is removed. | ||
* | ||
* Important: The actual DOM element given is assumed to exactly | ||
* represent the state of the `prev` tree. Since this function does NOT | ||
* track the real DOM at all, the resulting changes will result in | ||
* potentially undefined behavior if there're discrepancies. | ||
* | ||
* @param root | ||
* @param prev previous tree | ||
* @param curr current tree | ||
*/ | ||
function diffElement(root, prev, curr) { | ||
_diffElement(root, prev, curr, 0); | ||
} | ||
@@ -22,15 +41,16 @@ exports.diffElement = diffElement; | ||
const el = parent.children[child]; | ||
if (edits[0][0] !== 0 || prev[1].key !== curr[1].key || hasChangedEvents(prev[1], curr[1])) { | ||
let i, j, k, eq, e, status, idx, val; | ||
if (edits[0][0] !== 0 || (i = prev[1]).key !== (j = curr[1]).key || hasChangedEvents(i, j)) { | ||
// DEBUG && console.log("replace:", prev, curr); | ||
releaseDeep(prev); | ||
dom_1.removeChild(parent, child); | ||
dom_1.createDOM(parent, curr, undefined, child); | ||
dom_1.createDOM(parent, curr, child); | ||
return; | ||
} | ||
if (prev.__release && prev.__release !== curr.__release) { | ||
if ((i = prev.__release) && i !== curr.__release) { | ||
releaseDeep(prev); | ||
} | ||
if (curr.__init && prev.__init !== curr.__init) { | ||
if ((i = curr.__init) && i != prev.__init) { | ||
// DEBUG && console.log("call __init", curr); | ||
curr.__init.apply(curr, [el, ...(curr.__args)]); | ||
i.apply(curr, [el, ...(curr.__args)]); | ||
} | ||
@@ -43,4 +63,3 @@ if (edits[1][0] !== 0) { | ||
const noff = prev.length - 1; | ||
const offsets = []; | ||
let i, j, k, eq; | ||
const offsets = new Array(noff + 1); | ||
for (i = noff; i >= 2; i--) { | ||
@@ -50,3 +69,3 @@ offsets[i] = i - 2; | ||
for (i = 2; i < n; i++) { | ||
const e = edits[i], status = e[0], idx = e[1], val = e[2]; | ||
e = edits[i], status = e[0], val = e[2]; | ||
// DEBUG && console.log(`edit: o:[${offsets.toString()}] i:${idx} s:${status}`, val); | ||
@@ -63,2 +82,3 @@ if (status === -1) { | ||
else { | ||
idx = e[1]; | ||
// DEBUG && console.log("remove @", offsets[idx], val); | ||
@@ -83,4 +103,5 @@ releaseDeep(val); | ||
if (k === undefined || (k && equivKeys[k][0] === undefined)) { | ||
idx = e[1]; | ||
// DEBUG && console.log("insert @", offsets[idx], val); | ||
dom_1.createDOM(el, val, undefined, offsets[idx]); | ||
dom_1.createDOM(el, val, offsets[idx]); | ||
for (j = noff; j >= idx; j--) { | ||
@@ -128,10 +149,11 @@ offsets[j]++; | ||
function extractEquivElements(edits) { | ||
let k; | ||
let k, v, e, ek; | ||
const equiv = {}; | ||
for (let i = edits.length - 1; i >= 0; i--) { | ||
const e = edits[i]; | ||
const v = e[2]; | ||
e = edits[i]; | ||
v = e[2]; | ||
if (is_array_1.isArray(v) && (k = v[1].key) !== undefined) { | ||
equiv[k] = equiv[k] || [, ,]; | ||
equiv[k][e[0] + 1] = e[1]; | ||
ek = equiv[k]; | ||
!ek && (equiv[k] = ek = [, ,]); | ||
ek[e[0] + 1] = e[1]; | ||
} | ||
@@ -138,0 +160,0 @@ } |
15
dom.d.ts
@@ -1,2 +0,15 @@ | ||
export declare function createDOM(parent: Element, tag: any, opts?: any, insert?: number): any; | ||
/** | ||
* Creates an actual DOM tree from given hiccup component and `parent` | ||
* element. Calls `init` with created element (user provided context and | ||
* other args) for any components with `init` lifecycle method. Returns | ||
* created root element(s) - usually only a single one, but can be an | ||
* array of elements, if the provided tree is an iterable. Creates DOM | ||
* text nodes for non-component values. Returns `parent` if tree is | ||
* `null` or `undefined`. | ||
* | ||
* @param parent | ||
* @param tag | ||
* @param insert | ||
*/ | ||
export declare function createDOM(parent: Element, tag: any, insert?: number): any; | ||
export declare function createElement(parent: Element, tag: string, attribs?: any, insert?: number): HTMLElement | SVGElement; | ||
@@ -3,0 +16,0 @@ export declare function createTextElement(parent: Element, content: string, insert?: number): Text; |
29
dom.js
@@ -10,11 +10,24 @@ "use strict"; | ||
const map_1 = require("@thi.ng/iterators/map"); | ||
function createDOM(parent, tag, opts, insert) { | ||
/** | ||
* Creates an actual DOM tree from given hiccup component and `parent` | ||
* element. Calls `init` with created element (user provided context and | ||
* other args) for any components with `init` lifecycle method. Returns | ||
* created root element(s) - usually only a single one, but can be an | ||
* array of elements, if the provided tree is an iterable. Creates DOM | ||
* text nodes for non-component values. Returns `parent` if tree is | ||
* `null` or `undefined`. | ||
* | ||
* @param parent | ||
* @param tag | ||
* @param insert | ||
*/ | ||
function createDOM(parent, tag, insert) { | ||
if (is_array_1.isArray(tag)) { | ||
if (is_function_1.isFunction(tag[0])) { | ||
return createDOM(parent, tag[0].apply(null, tag.slice(1), opts)); | ||
const t = tag[0]; | ||
if (is_function_1.isFunction(t)) { | ||
return createDOM(parent, t.apply(null, tag.slice(1))); | ||
} | ||
const el = createElement(parent, tag[0], tag[1], insert); | ||
const el = createElement(parent, t, tag[1], insert); | ||
if (tag.__init) { | ||
const args = [el, ...(tag.__args)]; // Safari https://bugs.webkit.org/show_bug.cgi?format=multiple&id=162003 | ||
tag.__init.apply(tag, args); | ||
tag.__init.apply(tag, [el, ...tag.__args]); | ||
} | ||
@@ -24,3 +37,3 @@ if (tag[2]) { | ||
for (let i = 2; i < n; i++) { | ||
createDOM(el, tag[i], opts); | ||
createDOM(el, tag[i]); | ||
} | ||
@@ -31,3 +44,3 @@ } | ||
if (!is_string_1.isString(tag) && is_iterable_1.isIterable(tag)) { | ||
return [...(map_1.map((x) => createDOM(parent, x, opts), tag))]; | ||
return [...(map_1.map((x) => createDOM(parent, x), tag))]; | ||
} | ||
@@ -34,0 +47,0 @@ if (tag == null) { |
@@ -0,2 +1,60 @@ | ||
/** | ||
* Expands single hiccup element/component into its canonical form: | ||
* | ||
* ``` | ||
* [tagname, {attribs}, ...children] | ||
* ``` | ||
* | ||
* Emmet-style ID and class names in the original tagname are moved into | ||
* the attribs object, e.g.: | ||
* | ||
* ``` | ||
* ["div#foo.bar.baz"] => ["div", {id: "foo", class: "bar baz"}] | ||
* ``` | ||
* | ||
* If both Emmet-style classes AND a `class` attrib exists, the former | ||
* are appended to the latter: | ||
* | ||
* ``` | ||
* ["div.bar.baz", {class: "foo"}] => ["div", {class: "foo bar baz"}] | ||
* ``` | ||
* | ||
* @param spec | ||
* @param keys | ||
*/ | ||
export declare function normalizeElement(spec: any[], keys: boolean): any[]; | ||
export declare function normalizeTree(el: any, path?: number[], keys?: boolean, span?: boolean): any; | ||
/** | ||
* Calling this function is a prerequisite before passing a component | ||
* tree to `diffElement`. Recursively expands given hiccup component | ||
* tree into its canonical form by: | ||
* | ||
* - resolving Emmet-style tags (e.g. from `div#id.foo.bar`) | ||
* - evaluating embedded functions and replacing them with their result | ||
* - calling `render` life cycle method on component objects and using | ||
* result | ||
* - consuming iterables and normalizing results | ||
* - calling `deref()` on elements implementing `IDeref` interface and | ||
* using returned result | ||
* - calling `.toString()` on any other non-component value `x` and by | ||
* default wrapping it in `["span", x]`. The only exceptions to this | ||
* are: `option`, `textarea` and SVG `text` elements, for which spans | ||
* are always skipped. | ||
* | ||
* Additionally, unless `keys` is set to false, an unique `key` | ||
* attribute is created for each node in the tree. This attribute is | ||
* used by `diffElement` to determine if a changed node can be patched | ||
* or will need to be replaced/removed. The `key` values are defined by | ||
* the `path` array arg. | ||
* | ||
* For normal usage only the first 2 args should be specified and the | ||
* rest kept at their defaults. | ||
* | ||
* See `normalizeElement` for further details about canonical form. | ||
* | ||
* @param tree | ||
* @param ctx | ||
* @param path | ||
* @param keys | ||
* @param span | ||
*/ | ||
export declare function normalizeTree(tree: any, ctx?: any, path?: number[], keys?: boolean, span?: boolean): any; |
116
normalize.js
@@ -11,6 +11,28 @@ "use strict"; | ||
const api_1 = require("@thi.ng/hiccup/api"); | ||
/** | ||
* Expands single hiccup element/component into its canonical form: | ||
* | ||
* ``` | ||
* [tagname, {attribs}, ...children] | ||
* ``` | ||
* | ||
* Emmet-style ID and class names in the original tagname are moved into | ||
* the attribs object, e.g.: | ||
* | ||
* ``` | ||
* ["div#foo.bar.baz"] => ["div", {id: "foo", class: "bar baz"}] | ||
* ``` | ||
* | ||
* If both Emmet-style classes AND a `class` attrib exists, the former | ||
* are appended to the latter: | ||
* | ||
* ``` | ||
* ["div.bar.baz", {class: "foo"}] => ["div", {class: "foo bar baz"}] | ||
* ``` | ||
* | ||
* @param spec | ||
* @param keys | ||
*/ | ||
function normalizeElement(spec, keys) { | ||
let match, id, clazz, attribs; | ||
let tag = spec[0]; | ||
let hasAttribs = is_plain_object_1.isPlainObject(spec[1]) && !implements_function_1.implementsFunction(spec[1], "deref"); | ||
let tag = spec[0], hasAttribs = is_plain_object_1.isPlainObject(spec[1]), match, id, clazz, attribs; | ||
if (!is_string_1.isString(tag) || !(match = api_1.TAG_REGEXP.exec(tag))) { | ||
@@ -46,23 +68,60 @@ error_1.illegalArgs(`${tag} is not a valid tag name`); | ||
}; | ||
function normalizeTree(el, path = [0], keys = true, span = true) { | ||
if (el == null) { | ||
/** | ||
* Calling this function is a prerequisite before passing a component | ||
* tree to `diffElement`. Recursively expands given hiccup component | ||
* tree into its canonical form by: | ||
* | ||
* - resolving Emmet-style tags (e.g. from `div#id.foo.bar`) | ||
* - evaluating embedded functions and replacing them with their result | ||
* - calling `render` life cycle method on component objects and using | ||
* result | ||
* - consuming iterables and normalizing results | ||
* - calling `deref()` on elements implementing `IDeref` interface and | ||
* using returned result | ||
* - calling `.toString()` on any other non-component value `x` and by | ||
* default wrapping it in `["span", x]`. The only exceptions to this | ||
* are: `option`, `textarea` and SVG `text` elements, for which spans | ||
* are always skipped. | ||
* | ||
* Additionally, unless `keys` is set to false, an unique `key` | ||
* attribute is created for each node in the tree. This attribute is | ||
* used by `diffElement` to determine if a changed node can be patched | ||
* or will need to be replaced/removed. The `key` values are defined by | ||
* the `path` array arg. | ||
* | ||
* For normal usage only the first 2 args should be specified and the | ||
* rest kept at their defaults. | ||
* | ||
* See `normalizeElement` for further details about canonical form. | ||
* | ||
* @param tree | ||
* @param ctx | ||
* @param path | ||
* @param keys | ||
* @param span | ||
*/ | ||
function normalizeTree(tree, ctx, path = [0], keys = true, span = true) { | ||
if (tree == null) { | ||
return; | ||
} | ||
if (is_array_1.isArray(el)) { | ||
if (el.length === 0) { | ||
if (is_array_1.isArray(tree)) { | ||
if (tree.length === 0) { | ||
return; | ||
} | ||
const tag = el[0]; | ||
let norm; | ||
// use result of function call & pass remaining array elements as args | ||
const tag = tree[0]; | ||
let norm, nattribs; | ||
// use result of function call | ||
// pass ctx as first arg and remaining array elements as rest args | ||
if (is_function_1.isFunction(tag)) { | ||
return normalizeTree(tag.apply(null, el.slice(1)), path.slice(), keys, span); | ||
return normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, path.slice(), keys, span); | ||
} | ||
// component object w/ life cycle methods (render() is the only required hook) | ||
// component object w/ life cycle methods | ||
// (render() is the only required hook) | ||
if (implements_function_1.implementsFunction(tag, "render")) { | ||
const args = el.slice(1); | ||
norm = normalizeTree(tag.render.apply(null, args), path.slice(), keys, span); | ||
const args = [ctx, ...tree.slice(1)]; | ||
norm = normalizeTree(tag.render.apply(null, args), ctx, path.slice(), keys, span); | ||
if (norm !== undefined) { | ||
if (keys && norm[1].key === undefined) { | ||
norm[1].key = path.join("-"); | ||
nattribs = norm[1]; | ||
if (keys && nattribs.key === undefined) { | ||
nattribs.key = path.join("-"); | ||
} | ||
@@ -75,9 +134,10 @@ norm.__init = tag.init; | ||
} | ||
norm = normalizeElement(el, keys); | ||
if (keys && norm[1].key === undefined) { | ||
norm[1].key = path.join("-"); | ||
norm = normalizeElement(tree, keys); | ||
nattribs = norm[1]; | ||
if (keys && nattribs.key === undefined) { | ||
nattribs.key = path.join("-"); | ||
} | ||
if (norm.length > 2) { | ||
const tag = norm[0]; | ||
const res = [tag, norm[1]]; | ||
const res = [tag, nattribs]; | ||
span = span && !NO_SPANS[tag]; | ||
@@ -90,3 +150,3 @@ for (let i = 2, j = 2, k = 0, n = norm.length; i < n; i++) { | ||
for (let c of el) { | ||
c = normalizeTree(c, [...path, k], keys, span); | ||
c = normalizeTree(c, ctx, path.concat(k), keys, span); | ||
if (c !== undefined) { | ||
@@ -99,3 +159,3 @@ res[j++] = c; | ||
else { | ||
el = normalizeTree(el, [...path, k], keys, span); | ||
el = normalizeTree(el, ctx, path.concat(k), keys, span); | ||
if (el !== undefined) { | ||
@@ -112,12 +172,12 @@ res[j++] = el; | ||
} | ||
if (is_function_1.isFunction(el)) { | ||
return normalizeTree(el(), path, keys, span); | ||
if (is_function_1.isFunction(tree)) { | ||
return normalizeTree(tree(ctx), ctx, path, keys, span); | ||
} | ||
if (implements_function_1.implementsFunction(el, "deref")) { | ||
return normalizeTree(el.deref(), path.slice(), keys, span); | ||
if (implements_function_1.implementsFunction(tree, "deref")) { | ||
return normalizeTree(tree.deref(), ctx, path.slice(), keys, span); | ||
} | ||
return span ? | ||
["span", keys ? { key: path.join("-") } : {}, el.toString()] : | ||
el.toString(); | ||
["span", keys ? { key: path.join("-") } : {}, tree.toString()] : | ||
tree.toString(); | ||
} | ||
exports.normalizeTree = normalizeTree; |
{ | ||
"name": "@thi.ng/hdom", | ||
"version": "2.3.3", | ||
"version": "3.0.0", | ||
"description": "Lightweight vanilla ES6 UI component & virtual DOM system", | ||
@@ -19,3 +19,3 @@ "main": "./index.js", | ||
"devDependencies": { | ||
"@thi.ng/atom": "^1.2.3", | ||
"@thi.ng/atom": "^1.2.4", | ||
"@types/mocha": "^5.0.0", | ||
@@ -29,6 +29,6 @@ "@types/node": "^9.6.1", | ||
"dependencies": { | ||
"@thi.ng/api": "^2.1.3", | ||
"@thi.ng/diff": "^1.0.5", | ||
"@thi.ng/hiccup": "^1.3.3", | ||
"@thi.ng/iterators": "^4.1.3" | ||
"@thi.ng/api": "^2.2.0", | ||
"@thi.ng/diff": "^1.0.6", | ||
"@thi.ng/hiccup": "^1.3.4", | ||
"@thi.ng/iterators": "^4.1.4" | ||
}, | ||
@@ -35,0 +35,0 @@ "keywords": [ |
477
README.md
@@ -8,5 +8,30 @@ # @thi.ng/hdom | ||
**As of 2018-03-03 this package is now called @thi.ng/hdom, formerly | ||
@thi.ng/hiccup-dom** | ||
<!-- TOC depthFrom:2 depthTo:3 --> | ||
- [About](#about) | ||
- [Component tree translation](#component-tree-translation) | ||
- [Event & state handling options](#event--state-handling-options) | ||
- [Reusable components](#reusable-components) | ||
- [Status](#status) | ||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [User context injection](#user-context-injection) | ||
- [Component objects & life cycle methods](#component-objects--life-cycle-methods) | ||
- [Example projects](#example-projects) | ||
- [Dataflow graph SVG components](#dataflow-graph-svg-components) | ||
- [SPA with router and event bus](#spa-with-router-and-event-bus) | ||
- [Multiple apps with & without shared state](#multiple-apps-with--without-shared-state) | ||
- [Interceptor based event handling](#interceptor-based-event-handling) | ||
- [Todo list](#todo-list) | ||
- [Cellular automata](#cellular-automata) | ||
- [SVG particles](#svg-particles) | ||
- [JSON based components](#json-based-components) | ||
- [@thi.ng/rstream dataflow graph](#thingrstream-dataflow-graph) | ||
- [Basic usage patterns](#basic-usage-patterns) | ||
- [Benchmark](#benchmark) | ||
- [Authors](#authors) | ||
- [License](#license) | ||
<!-- /TOC --> | ||
## About | ||
@@ -18,4 +43,2 @@ | ||
Benefits: | ||
- Use the full expressiveness of ES6/TypeScript to define, annotate & | ||
@@ -25,16 +48,18 @@ document components | ||
- No pre-processing / pre-compilation steps | ||
- No string parsing / interpolation steps | ||
- Less verbose than HTML, resulting in smaller file sizes | ||
- Static components can be distributed as JSON (or [dynamically compose | ||
components, based on JSON | ||
data](https://github.com/thi-ng/umbrella/tree/master/examples/json-components)) | ||
- Supports SVG, arbitrary elements, attributes, events | ||
- CSS conversion from JS objects | ||
- Less verbose than HTML/JSX, resulting in smaller file sizes | ||
- Static components can be distributed as JSON (or [transform JSON | ||
into components](https://github.com/thi-ng/umbrella/tree/master/examples/json-components)) | ||
- Optional user context injection (an arbitrary object passed to all | ||
component functions) | ||
- auto-deref of embedded value wrappers which implement the | ||
`@thi.ng/api/IDeref` interface (e.g. atoms, cursors, derived views, | ||
streams etc.) | ||
- CSS conversion from JS objects for `style` attribs | ||
- Suitable for server side rendering (by passing the same data structure | ||
to @thi.ng/hiccup's `serialize()`) | ||
- Fairly fast (see benchmark example below) | ||
- Only ~10KB minified | ||
- Only ~4.4KB gzipped | ||
```typescript | ||
import * as hiccup from "@thi.ng/hiccup"; | ||
import * as hdom from "@thi.ng/hdom"; | ||
@@ -62,21 +87,30 @@ | ||
hdom.createDOM(document.body, hdom.normalizeTree(app())); | ||
``` | ||
// alternatively browser or server side HTML serialization | ||
// (note: does not emit attributes w/ functions as values, i.e. the button "onclick" attribs) | ||
console.log(hiccup.serialize(app())); | ||
[Live demo](http://demo.thi.ng/umbrella/hdom-basics/) | [standalone example](https://github.com/thi-ng/umbrella/tree/master/examples/hdom-basics) | ||
Alternatively, use the same component function for browser or server | ||
side HTML serialization (Note: does not emit attributes w/ functions as | ||
values, e.g. a button's `onclick` attrib). | ||
```ts | ||
import { serialize } from "@thi.ng/hiccup"; | ||
console.log(serialize(app())); | ||
// <div id="app"><h1 class="title">hello world</h1><button>clicks: 0</button><button>clicks: 100</button></div> | ||
``` | ||
[Live demo](http://demo.thi.ng/umbrella/hdom-basics/) | [standalone example](https://github.com/thi-ng/umbrella/tree/master/examples/hdom-basics) | ||
No template engine & no precompilation steps needed, just use the full | ||
expressiveness of ES6/TypeScript to define your DOM tree. The additional | ||
benefit of using TypeScript is that your UI components can become | ||
strongly typed, since they're just normal functions, can use generics, | ||
No template engine & no pre-compilation steps needed, just use the full | ||
expressiveness of ES6/TypeScript to define your DOM tree. Using | ||
TypeScript gives the additional benefit of making UI components strongly | ||
typed, and since they're just normal functions, can use generics, | ||
overrides, varargs etc. | ||
### Component tree translation | ||
The actual DOM update is based on the minimal edit set of the recursive | ||
difference between the old and new DOM trees (both nested JS arrays). | ||
Components can be defined as static arrays, closures or objects with | ||
life cycle hooks (init, render, release). | ||
difference between the old and new DOM trees (both expressed as nested | ||
JS arrays). Components can be defined as static arrays, closures or | ||
objects with [life cycle methods](#lifecycle-methods) (init, render, | ||
release). | ||
@@ -89,18 +123,33 @@ ![hdom dataflow](../../assets/hdom-dataflow.svg) | ||
latter is a wrapper around React, whereas this library is standalone, | ||
more lowlevel & less opinionated. | ||
more low-level & less opinionated. | ||
If you're interested in using this, please also consider the | ||
[@thi.ng/atom](https://github.com/thi-ng/umbrella/tree/master/packages/atom) | ||
and | ||
[@thi.ng/rstream](https://github.com/thi-ng/umbrella/tree/master/packages/rstream) | ||
packages to integrate app state handling, event streams & reactive value | ||
subscriptions. More examples are forthcoming... | ||
### Event & state handling options | ||
Since this package is purely dealing with the translation of DOM trees, | ||
any form of state / event handling or routing required by a full app is | ||
out of scope. These features are provided by the following packages and | ||
can be used in a mix & match manner: | ||
- [@thi.ng/atom](https://github.com/thi-ng/umbrella/tree/master/packages/atom) | ||
- [@thi.ng/interceptors](https://github.com/thi-ng/umbrella/tree/master/packages/interceptors) | ||
- [@thi.ng/router](https://github.com/thi-ng/umbrella/tree/master/packages/router) | ||
- [@thi.ng/rstream](https://github.com/thi-ng/umbrella/tree/master/packages/rstream) | ||
- [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/master/packages/transducers) | ||
### Reusable components | ||
A currently small (but growing) number of reusable components are | ||
provided by these packages: | ||
- [@thi.ng/hdom-components](https://github.com/thi-ng/umbrella/tree/master/packages/hdom-components) | ||
- [@thi.ng/hiccup-svg](https://github.com/thi-ng/umbrella/tree/master/packages/hiccup-svg) | ||
## Status | ||
This project is currently still in BETA. The overall "API" is stable, | ||
but there's still further work planned on optimization and | ||
generalization beyond the standard browser DOM use cases. Furthermore, | ||
the project has been used for several projects in production since 2016. | ||
The overall "API" is stable, but there's further work planned on | ||
generalizing the approach beyond standard browser DOM use cases (planned | ||
for v4.0.0). The project has been used for several projects in | ||
production since 2016. | ||
## Installation | ||
@@ -112,6 +161,6 @@ | ||
**New since 2018-03-15: You can now create a preconfigured app skeleton | ||
using @thi.ng/atom, @thi.ng/hdom & @thi.ng/router using the | ||
Use the customizable | ||
[create-hdom-app](https://github.com/thi-ng/create-hdom-app) project | ||
generator:** | ||
generator to create a pre-configured app skeleton using @thi.ng/atom, | ||
@thi.ng/hdom, @thi.ng/interceptors & @thi.ng/router: | ||
@@ -126,88 +175,319 @@ ``` | ||
## Usage examples | ||
## Usage | ||
Even though the overall approach should be obvious from the code | ||
examples below, it's recommended to first study the | ||
examples in this document, it's recommended to first study the | ||
[@thi.ng/hiccup](https://github.com/thi-ng/umbrella/tree/master/packages/hiccup) | ||
reference. It's also important to point out, that this project | ||
**currently** has some differences as to how some attribute and | ||
iterables are treated and/or are supported in general. This project also | ||
has additional features (e.g. life cycle hooks), which aren't needed for | ||
the static serialization use cases of hiccup. Both experiments started | ||
in early 2016, but have somewhat evolved independently and require some | ||
conceptional synchronization. | ||
reference to learn about the basics of the approach and syntax used. | ||
Compared to @thi.ng/hiccup, this project has additional features (e.g. | ||
life cycle hooks), which aren't needed for the static serialization use | ||
cases of @thi.ng/hiccup. Both projects started in early 2016, but have | ||
somewhat evolved independently. | ||
### Dataflow graph SVG components | ||
#### `start(parent: Element | string, tree: any, ctx?: any, path?: number[], keys?: boolean, span?: boolean): () => boolean` | ||
This is a preview of the upcoming | ||
[@thi.ng/estuary](https://github.com/thi-ng/umbrella/tree/feature/estuary/packages/estuary) | ||
package: | ||
Main user function of this package. For most use cases, this function | ||
should be the only one required. It takes a parent DOM element (or ID), | ||
hiccup tree (array, function or component object w/ life cycle methods) | ||
and an optional arbitrary context object. Starts RAF update loop, in | ||
each iteration first normalizing given tree, then computing diff to | ||
previous frame's tree and applying any changes to the real DOM. The | ||
optional `context` arg can be used for passing global config data or | ||
state down into the hiccup component tree. Any embedded component | ||
function in the tree will receive this context object as first argument, | ||
as will life cycle methods in component objects. See [context | ||
description](#user-context) further below. | ||
[Source](https://github.com/thi-ng/umbrella/tree/feature/estuary/packages/estuary) | [Live demo](http://demo.thi.ng/umbrella/estuary/) | ||
**Selective updates**: No updates will be applied if the given hiccup | ||
tree is `undefined` or `null` or a root component function returns no | ||
value. This way a given root function can do some state handling of its | ||
own and implement fail-fast checks to determine no DOM updates are | ||
necessary, saving effort re-creating a new hiccup tree and request | ||
skipping DOM updates via this function. In this case, the previous DOM | ||
tree is kept around until the root function returns a tree again, which | ||
then is diffed and applied against the previous tree kept as usual. Any | ||
number of frames may be skipped this way. | ||
### Todo list | ||
**Important:** The parent element given is assumed to have NO children at | ||
the time when `start()` is called. Since hdom does NOT track the real | ||
DOM, the resulting changes will result in potentially undefined behavior | ||
if the parent element wasn't empty. | ||
A fully documented todo list app with undo / redo feature is here: | ||
Returns a function, which when called, immediately cancels the update | ||
loop. | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/todo-list) | [Live demo](http://demo.thi.ng/umbrella/todo-list/) | ||
#### `normalizeTree(tree: any, ctx?: any): any` | ||
### Cellular automata | ||
Calling this function is a prerequisite before passing a component tree | ||
to `diffElement`. Recursively expands given hiccup component tree into | ||
its canonical form by: | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/cellular-automata) | [Live demo](http://demo.thi.ng/umbrella/cellular-automata/) | ||
- resolving Emmet-style tags (e.g. from `div#id.foo.bar`) | ||
- evaluating embedded functions and replacing them with their result | ||
- calling `render` life cycle method on component objects and using | ||
result | ||
- consuming iterables and normalizing results | ||
- calling `deref()` on elements implementing `IDeref` interface and | ||
using returned result | ||
- calling `.toString()` on any other non-component value `x` and by | ||
default wrapping it in `["span", x]`. The only exceptions to this are: | ||
`option`, `textarea` and SVG `text` elements, for which spans are | ||
always skipped. | ||
### SVG particles | ||
Additionally, unless `keys` is set to false, an unique `key` attribute | ||
is created for each node in the tree. This attribute is used by | ||
`diffElement` to determine if a changed node can be patched or will need | ||
to be replaced/removed. The `key` values are defined by the `path` array | ||
arg. | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/svg-particles) | [Live demo](http://demo.thi.ng/umbrella/svg-particles/) | ||
For normal usage only the first 2 args should be specified and the rest | ||
kept at their defaults. | ||
### JSON based components | ||
#### `diffElement(parent: Element, prev: any, curr: any): void` | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/json-components) | [Live demo](http://demo.thi.ng/umbrella/json-components/) | ||
Takes a DOM root element and two hiccup trees, `prev` and `curr`. | ||
Recursively computes diff between both trees and applies any necessary | ||
changes to reflect `curr` tree in real DOM. | ||
### Basic usage patterns | ||
For newly added components, calls `init` with created DOM element (plus | ||
user provided context and any other args) for any components with `init` | ||
life cycle method. Likewise, calls `release` on components with | ||
`release` method when the DOM element is removed. | ||
The code below is also available as standalone project in: [/examples/dashboard](https://github.com/thi-ng/umbrella/tree/master/examples/dashboard) | ||
**Important:** The actual DOM element/subtree given is assumed to | ||
exactly represent the state of the `prev` tree. Since this function does | ||
NOT track the real DOM at all, the resulting changes will result in | ||
potentially undefined behavior if there're discrepancies. | ||
[Live demo here](http://demo.thi.ng/umbrella/dashboard/) | ||
#### `createDOM(parent: Element, tag: any, insert?: number): any` | ||
```typescript | ||
Creates an actual DOM tree from given hiccup component and `parent` | ||
element. Calls `init` with created element (user provided context and | ||
other args) for any components with `init` life cycle method. Returns | ||
created root element(s) - usually only a single one, but can be an array | ||
of elements, if the provided tree is an iterable. Creates DOM text nodes | ||
for non-component values. Returns `parent` if tree is `null` or | ||
`undefined`. | ||
### User context injection | ||
Since v3.0.0 hdom offers support for an arbitrary "context" object | ||
passed to `start()`, and then automatically injected as argument to | ||
**all** component function calls anywhere in the tree. This avoids | ||
having to manually pass down configuration data into each sub-component | ||
and so can simplify certain use cases, e.g. event dispatch, style | ||
information, global state etc. | ||
```ts | ||
import { start } from "@thi.ng/hdom"; | ||
import { Event, EventBus } from "@thi.ng/interceptors"; | ||
// static component function to create styled box | ||
const box = (prefix, body) => | ||
["div", | ||
// (optional) type aliases to better illustrate demo context structure | ||
type AppContext = { | ||
bus: EventBus, | ||
ui: { link: string, list: string } | ||
}; | ||
type LinkSpec = [Event, any]; | ||
// user defined context object | ||
// should include whatever config is required by your components | ||
const ctx: AppContext = { | ||
// event processor from @thi.ng/interceptors | ||
bus: new EventBus(), | ||
// component styling (using Tachyons CSS) | ||
ui: { | ||
link: "fw7 blue link dim pointer", | ||
list: "list center tc" | ||
} | ||
}; | ||
// link component with `onclick` handler, which dispatches `evt` | ||
// on EventBus obtained from context | ||
// `ctx` arg is automatically provided when component is called | ||
const eventLink = (ctx: AppContext, evt: Event, ...body: any[]) => | ||
["a", | ||
{ | ||
style: { | ||
display: "inline-block", | ||
background: "#ccc", | ||
width: "30%", | ||
height: "40px", | ||
padding: "4px", | ||
margin: "2px", | ||
"text-align": "center" | ||
} | ||
class: ctx.ui.link, | ||
onclick: () => ctx.bus.dispatch(evt), | ||
}, | ||
["strong", prefix], ["br"], body]; | ||
...body]; | ||
// stateful component function | ||
const counter = (id, from = 0, step = 1) => () => box(id, (from += step).toLocaleString()); | ||
// dynamic component function (external state, i.e. date) | ||
const timer = () => box("time", new Date().toLocaleTimeString()); | ||
// list component wrapper for links | ||
const linkList = (ctx: AppContext, ...links: LinkSpec[]) => | ||
["ul", { class: ctx.ui.list }, | ||
links.map((l) => ["li", [eventLink, ...l]])]; | ||
// application root component closure | ||
// initializes stateful components | ||
const app = (() => { | ||
const users = counter("users"); | ||
const profits = counter("$$$", 1e6, 99); | ||
return () => ["div", ["h1", "Dashboard"], users, profits, timer]; | ||
})(); | ||
// root component | ||
// i.e. creates list of of provided dummy event link specs | ||
const root = [ | ||
linkList, | ||
[["handle-login"], "Login"], | ||
[["external-link", "http://thi.ng"], "thi.ng"], | ||
]; | ||
// start update loop (RAF) | ||
window.addEventListener("load", () => start("app", app)); | ||
// start hdom update loop | ||
start("app", root, ctx); | ||
``` | ||
### @thi.ng/rstream integration | ||
### Component objects & life cycle methods | ||
TODO example forthcoming... | ||
Most components can be succinctly expressed via vanilla JS functions, | ||
though for some use cases we need to get a handle on the actual | ||
underlying DOM element and can only fully initialize the component once | ||
it's been mounted etc. For those cases components can be specified as | ||
classes or plain objects implementing the following interface: | ||
```ts | ||
interface ILifecycle { | ||
/** | ||
* Component init method. Called with the actual DOM element, | ||
* hdom user context and any other args when the component is | ||
* first used, but **after** `render()` has been called once already. | ||
*/ | ||
init?(el: Element, ctx: any, ...args: any[]); | ||
/** | ||
* Returns the hdom tree of this component. | ||
* Note: Always will be called first (prior to `init`/`release`). | ||
* Therefore might have to include checks if any local state | ||
* has already been initialized via `init`. This is the only | ||
* mandatory method which MUST be implemented. | ||
* | ||
* `render` is executed before `init` because `normalizeTree()` | ||
* must obtain the component's hdom tree first before it can | ||
* determine if an `init` is necessary. `init` itself will be | ||
* called from `diffElement` (or `createDOM`) in a later | ||
* phase of processing. | ||
*/ | ||
render(ctx: any, ...args: any[]): any; | ||
/** | ||
* Called when the underlying DOM of this component is removed | ||
* (or replaced). Intended for cleanup tasks. | ||
*/ | ||
release?(ctx: any, ...args: any[]); | ||
} | ||
``` | ||
When the component is first used the order of execution is: `render` -> | ||
`init`. The `release` method is called when the component has been | ||
removed / replaced (basically if it's not present in the new tree | ||
anymore). `release` should NOT manually call `release` on any children, | ||
since that's already handled by `diffElement()`. | ||
The rest `...args` provided are sourced from the component call site as | ||
this simple example demonstrates: | ||
```ts | ||
// wrap in closure to allow multiple instances | ||
const canvas = () => { | ||
return { | ||
init: (el, ctx, { width, height }, msg, color = "red") => { | ||
const c = el.getContext("2d"); | ||
c.fillStyle = color; | ||
c.fillRect(0, 0, width, height); | ||
c.fillStyle = "white"; | ||
c.textAlign = "center"; | ||
c.fillText(msg, width / 2, height / 2); | ||
}, | ||
render: (ctx, attribs) => ["canvas", attribs], | ||
}; | ||
}; | ||
// usage scenario #1: static component | ||
// inline initialization is okay here... | ||
start( | ||
document.body, | ||
[canvas(), { width: 100, height: 100 }, "Hello world"] | ||
); | ||
// usage scenario #2: dynamic component | ||
// in this example, the root component itself is given as function, which | ||
// is evaluated each frame | ||
// since `canvas()` is a higher order component it too produces a new instance | ||
// with each call. therefore the canvas instance(s) need to be created beforehand | ||
const app = () => { | ||
// pre-instantiate canvases | ||
let c1 = canvas(); | ||
let c2 = canvas(); | ||
// return root component function | ||
return () => ["div", | ||
// some dynamic other content | ||
["p", new Date().toString()], | ||
// use canvas instances | ||
[c1, { width: 100, height: 100 }, "Hello world"], | ||
[c2, { width: 100, height: 100 }, "Goodbye world", "blue"] | ||
]; | ||
}; | ||
start(document.body, app()); | ||
``` | ||
## Example projects | ||
Most of the | ||
[examples](https://github.com/thi-ng/umbrella/tree/master/examples) | ||
included in this repo are using this package in one way or another. | ||
Please check them out to learn more. Each is heavily commented, incl. | ||
best practice notes. | ||
### Dataflow graph SVG components | ||
This is a preview of the upcoming | ||
[@thi.ng/estuary](https://github.com/thi-ng/umbrella/tree/feature/estuary/packages/estuary) | ||
package: | ||
[Source](https://github.com/thi-ng/umbrella/tree/feature/estuary/packages/estuary) | [Live version](http://demo.thi.ng/umbrella/estuary/) | ||
### SPA with router and event bus | ||
Based on the `create-hdom-app` project scaffolding, this is one of the | ||
more advanced demos, combining functionality of several other @thi.ng | ||
packages. | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/router-basics) | [Live version](http://demo.thi.ng/umbrella/router-basics/) | ||
### Multiple apps with & without shared state | ||
Devcards style BMI calculator(s) with basic SVG viz. | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/devcards) | [Live version](http://demo.thi.ng/umbrella/devcards/) | ||
### Interceptor based event handling | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/interceptor-basics) | [Live version](http://demo.thi.ng/umbrella/interceptor-basics/) | ||
### Todo list | ||
A fully documented, obligatory todo list app with undo / redo. | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/todo-list) | [Live version](http://demo.thi.ng/umbrella/todo-list/) | ||
### Cellular automata | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/cellular-automata) | [Live version](http://demo.thi.ng/umbrella/cellular-automata/) | ||
### SVG particles | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/svg-particles) | [Live version](http://demo.thi.ng/umbrella/svg-particles/) | ||
### JSON based components | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/json-components) | [Live version](http://demo.thi.ng/umbrella/json-components/) | ||
### @thi.ng/rstream dataflow graph | ||
A small, interactive dataflow graph example: | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/rstream-dataflow) | [Live version](http://demo.thi.ng/umbrella/rstream-dataflow) | ||
### Basic usage patterns | ||
The code below is also available as standalone project in: [/examples/dashboard](https://github.com/thi-ng/umbrella/tree/master/examples/dashboard) | ||
[Source](https://github.com/thi-ng/umbrella/tree/master/examples/dashboard) | [Live version](http://demo.thi.ng/umbrella/dashboard/) | ||
### Benchmark | ||
@@ -218,3 +498,3 @@ | ||
[Live demo here](http://demo.thi.ng/umbrella/hdom-benchmark/) | ||
[Live version](http://demo.thi.ng/umbrella/hdom-benchmark/) | ||
@@ -224,6 +504,7 @@ Based on [user feedback collected via | ||
performance should be more than acceptable for even quite demanding UIs. | ||
In the 192/256 cells configurations this stress test causes approx. | ||
600/800 DOM every single frame, something very unlikely for a typical | ||
web app. In Chrome 64 on a MBP2016 this still runs at a pretty stable | ||
30fps (50 frame SMA). | ||
In the 192 / 256 cells configurations **this stress test causes approx. | ||
600 / 800 DOM every single frame**, very unlikely for a typical web app. | ||
In Chrome 64 on a MBP2016 this still runs at a stable 60fps (192 cells) | ||
/ 32fps (256 cells). Both FPS readings based the 50 frame | ||
[SMA](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average). | ||
@@ -230,0 +511,0 @@ ## Authors |
/** | ||
* Takes a parent DOM element (or ID) and hiccup tree | ||
* (array or function) and starts RAF update loop, | ||
* computing diff to previous frame's tree and applying | ||
* any changes to the real DOM. | ||
* Takes a parent DOM element (or ID), hiccup tree (array, function or | ||
* component object w/ lifecycle methods) and an optional context | ||
* object. Starts RAF update loop, computing diff to previous frame's | ||
* tree and applying any changes to the real DOM. | ||
* | ||
* **Selective updates**: No updates will be applied | ||
* if the given hiccup tree is `undefined` or `null` or | ||
* a root component function returns no value. This way | ||
* a given root function can do some state handling of its own | ||
* and implement fail-fast checks to determine no DOM updates | ||
* are necessary, save effort re-creating a new hiccup tree and | ||
* request skipping DOM updates via this function. In this case, | ||
* the previous DOM tree is kept around until the root function | ||
* returns a tree again, which then is diffed and applied against | ||
* the previous tree kept as usual. Any number of frames can be | ||
* skipped this way. | ||
* The optional `context` arg can be used for passing global config data | ||
* or state down into the hiccup component tree. Any embedded component | ||
* function in the tree will receive this context object as first | ||
* argument, as will life cycle methods in component objects. | ||
* | ||
* Important: The parent element given is assumed to have NO | ||
* children at the time when `start()` is called. Since | ||
* hdom does NOT track the real DOM, the resulting | ||
* changes will result in potentially undefined behavior | ||
* if the parent element wasn't empty. | ||
* **Selective updates**: No updates will be applied if the given hiccup | ||
* tree is `undefined` or `null` or a root component function returns no | ||
* value. This way a given root function can do some state handling of | ||
* its own and implement fail-fast checks to determine no DOM updates | ||
* are necessary, save effort re-creating a new hiccup tree and request | ||
* skipping DOM updates via this function. In this case, the previous | ||
* DOM tree is kept around until the root function returns a tree again, | ||
* which then is diffed and applied against the previous tree kept as | ||
* usual. Any number of frames may be skipped this way. | ||
* | ||
* Returns a function, which when called, immediately | ||
* cancels the update loop. | ||
* Important: The parent element given is assumed to have NO children at | ||
* the time when `start()` is called. Since hdom does NOT track the real | ||
* DOM, the resulting changes will result in potentially undefined | ||
* behavior if the parent element wasn't empty. | ||
* | ||
* Returns a function, which when called, immediately cancels the update | ||
* loop. | ||
* | ||
* @param parent root element or ID | ||
* @param tree hiccup DOM tree | ||
* @param ctx arbitrary user context object | ||
* @param spans true (default), if text should be wrapped in `<span>` | ||
*/ | ||
export declare function start(parent: Element | string, tree: any, spans?: boolean): () => boolean; | ||
export declare function start(parent: Element | string, tree: any, ctx?: any, spans?: boolean): () => boolean; |
51
start.js
@@ -7,33 +7,36 @@ "use strict"; | ||
/** | ||
* Takes a parent DOM element (or ID) and hiccup tree | ||
* (array or function) and starts RAF update loop, | ||
* computing diff to previous frame's tree and applying | ||
* any changes to the real DOM. | ||
* Takes a parent DOM element (or ID), hiccup tree (array, function or | ||
* component object w/ lifecycle methods) and an optional context | ||
* object. Starts RAF update loop, computing diff to previous frame's | ||
* tree and applying any changes to the real DOM. | ||
* | ||
* **Selective updates**: No updates will be applied | ||
* if the given hiccup tree is `undefined` or `null` or | ||
* a root component function returns no value. This way | ||
* a given root function can do some state handling of its own | ||
* and implement fail-fast checks to determine no DOM updates | ||
* are necessary, save effort re-creating a new hiccup tree and | ||
* request skipping DOM updates via this function. In this case, | ||
* the previous DOM tree is kept around until the root function | ||
* returns a tree again, which then is diffed and applied against | ||
* the previous tree kept as usual. Any number of frames can be | ||
* skipped this way. | ||
* The optional `context` arg can be used for passing global config data | ||
* or state down into the hiccup component tree. Any embedded component | ||
* function in the tree will receive this context object as first | ||
* argument, as will life cycle methods in component objects. | ||
* | ||
* Important: The parent element given is assumed to have NO | ||
* children at the time when `start()` is called. Since | ||
* hdom does NOT track the real DOM, the resulting | ||
* changes will result in potentially undefined behavior | ||
* if the parent element wasn't empty. | ||
* **Selective updates**: No updates will be applied if the given hiccup | ||
* tree is `undefined` or `null` or a root component function returns no | ||
* value. This way a given root function can do some state handling of | ||
* its own and implement fail-fast checks to determine no DOM updates | ||
* are necessary, save effort re-creating a new hiccup tree and request | ||
* skipping DOM updates via this function. In this case, the previous | ||
* DOM tree is kept around until the root function returns a tree again, | ||
* which then is diffed and applied against the previous tree kept as | ||
* usual. Any number of frames may be skipped this way. | ||
* | ||
* Returns a function, which when called, immediately | ||
* cancels the update loop. | ||
* Important: The parent element given is assumed to have NO children at | ||
* the time when `start()` is called. Since hdom does NOT track the real | ||
* DOM, the resulting changes will result in potentially undefined | ||
* behavior if the parent element wasn't empty. | ||
* | ||
* Returns a function, which when called, immediately cancels the update | ||
* loop. | ||
* | ||
* @param parent root element or ID | ||
* @param tree hiccup DOM tree | ||
* @param ctx arbitrary user context object | ||
* @param spans true (default), if text should be wrapped in `<span>` | ||
*/ | ||
function start(parent, tree, spans = true) { | ||
function start(parent, tree, ctx, spans = true) { | ||
let prev = []; | ||
@@ -46,3 +49,3 @@ let isActive = true; | ||
if (isActive) { | ||
const curr = normalize_1.normalizeTree(tree, [0], true, spans); | ||
const curr = normalize_1.normalizeTree(tree, ctx, [0], true, spans); | ||
if (curr != null) { | ||
@@ -49,0 +52,0 @@ diff_1.diffElement(parent, prev, curr); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
67645
771
509
Updated@thi.ng/api@^2.2.0
Updated@thi.ng/diff@^1.0.6
Updated@thi.ng/hiccup@^1.3.4
Updated@thi.ng/iterators@^4.1.4