Socket
Socket
Sign inDemoInstall

@thi.ng/hdom

Package Overview
Dependencies
Maintainers
1
Versions
273
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@thi.ng/hdom - npm Package Compare versions

Comparing version 2.3.3 to 3.0.0

6

api.d.ts
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;

@@ -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 @@ }

@@ -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;

@@ -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;

@@ -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": [

@@ -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;

@@ -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);

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc