{{pkg.description}}
In many ways this package is the direct successor of
@thi.ng/hdom,
which for several years was my preferred way of building UIs. hdom eschewed
using a virtual DOM to represent and maintain a dynamic tree of (UI) components
and instead only required a previous and current component tree in
@thi.ng/hiccup
format (aka nested, plain JS arrays w/ optional support for embedded other JS
data types, like ES6 iterables, @thi.ng/api
interfaces, etc.)
to perform its UI updates. Yet, whilst hiccup trees are plain, simple, user
defined data structures, which can be very easily composed without any
libraries, hdom itself was still heavily influenced by the general vDOM
approach and therefore a centralized update cycle and computing differences
between the trees were necessary evils core tasks. In short, hdom allowed
the illusion of declarative components with reactive state updates, but had to
use a complex and recursive diff to realize those updates.
In contrast, @thi.ng/rdom directly supports embedding reactive values/components in the hiccup tree and compiles them in such a way that their value changes directly target underlying DOM nodes without having to resort to any other intermediate processing (no diffing, vDOM updates etc.). @thi.ng/rdom is entirely vDOM-free. It supports declarative component definitions via @thi.ng/hiccup, @thi.ng/rstream, ES6 classes, direct DOM manipulation (incl. provided helpers) and/or any mixture of these approaches.
If a reactive value is used for an element attribute, a value change will
trigger an update of only that attribute (there's special handling for event
listeners, CSS classes, data attributes and style
attribs). If a reactive
value is used as (text) body of an element (or an element/component itself),
only that body/subtree in the target DOM will be impacted/updated directly...
The package provides an interface
IComponent
(with a super simple life cycle API), a base component class
Component
for
stubbing and a number of fundamental control constructs & component-wrappers for
composing more complex components and to reduce boilerplate for various
situations. Whilst targetting a standard JS DOM by default, each component can
decide for itself what kind of target data structure (apart from a browser DOM)
it manages. rdom components themselves have no mandatory knowledge of a
browser DOM. As an example, similar to
@thi.ng/hdom-canvas,
the
@thi.ng/rdom-canvas
wrapper provides a component which subscribes to a stream of hiccup-based scene
descriptions (trees) and then translates each scene-value into HTML Canvas API
draw calls.
Since there's no central coordination in rdom (neither explicitly nor implicitly), each component can (and does) update whenever its state value has changed. Likewise, components are free to directly manipulate the DOM through other means, as hinted at earlier.
The IComponent
interface is at the heart of rdom. It defines three lifecycle methods to:
.mount()
, .unmount()
and .update()
a component. The first two are always
async
to allow for more complex component initialization procedures (e.g.
preloaders, WASM init, other async ops...). Several of the higher-order
controller components/constructs too demand async
functions for the same
reasons.
Because rdom itself relies for most reactive features, stream composition and
reactive value transformations on other packages, i.e.
@thi.ng/rstream,
@thi.ng/transducers-async
and
@thi.ng/transducers,
please consult the docs for these packages to learn more about the available
constructs and patterns. Most of rdom only deals with either subscribing to
reactive values, async iterables and/or wrapping/transforming existing
subscriptions, either explicitly using the provided control components (e.g.
$async()
),
$sub()
, or using
$compile()
to
auto-wrap such values embedded in an hiccup tree.
For the sake of deduplication of functionality and to keep the number of dependencies to a minimum, direct @thi.ng/atom integration has been removed in favor of using relevant @thi.ng/rstream constructs, which can be used as lightweight adapters, i.e.:
The package provides many functions to simplify the creation of individual or entire trees of DOM elements and to manipulate them at a later time. The single most important function of the package is $compile. It acts as a facade for many of these other functions and creates an actual DOM from a given hiccup component tree. It also automatically wraps any reactive values contained therein.
All of the following functions are also usable, even if you don't intend to use any other package features!
For more advanced usage, rdom provides a range of control structures (container components) to simplify the handling of reactive states and reduce boilerplate for the implementation of common UI structures (e.g. item lists of any kind).
The following links lead to the documentation of these wrappers, incl. small code examples:
- $async
- $klist
- $list
- $lazy
- $object
- $promise
- $refresh
- $replace
- $sub
- $subObject
- $switch
- $wrapEl
- $wrapHtml
- $wrapText
Currently, reactive rdom components are based on @thi.ng/rstream subscriptions. To create a feedback loop between those reactive state values and their subscribed UI components, input event handlers need to feed any user changes back to those reactive state(s). To reduce boilerplate for these tasks, the following higher order input event handlers are provided:
import { $compile, $input } from "@thi.ng/rdom";
import { reactive, trace } from "@thi.ng/rstream";
// reactive value/state w/ transformation
const name = reactive("").map((x) => x.toUpperCase());
// reactive text field for `name`
$compile(["input", {
type: "text",
// any value changes are fed back into `name`, which in return
// triggers an update of this (and any other) subscription
oninput: $input(name),
value: name
}]).mount(document.body);
// addtional subscription for debug console output
name.subscribe(trace("name:"));
Click counter using thi.ng/rstream and thi.ng/transducers:
import { $compile, $inputTrigger } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
import { count, scan } from "@thi.ng/transducers";
// reactive value/stream setup
const clicks = reactive(true);
// button component with reactive label showing click count
$compile([
"button",
// $inputTrigger merely emits `true` onto the given reactive stream
{ onclick: $inputTrigger(clicks) },
"clicks: ",
// using transducers to transform click stream into a counter
clicks.transform(scan(count(-1))),
]).mount(document.body);
Work is underway to better support built-in AsyncIterables (possibly entirely in-lieu of rstream constructs). Currently, they can only be directly used for simple text or attribute values (also see the rdom-async example):
import { $compile } from "@thi.ng/rdom";
import { range, source } from "@thi.ng/transducers-async";
// infinite 1Hz counter
const counter = range(1000);
// manually updated click counter (an async iterable with extended API)
// see: https://docs.thi.ng/umbrella/transducers-async/functions/source-1.html
const clicks = source(0);
// event handler to update click count
const updateClicks = () => clicks.update((x)=> x + 1);
// compile DOM with embedded async iterables
$compile(
["div", {},
["div", {}, "counter: ", counter],
["button", { onclick: updateClicks }, "clicks: ", clicks]
]
).mount(document.body)
{{meta.status}}
{{repo.supportPackages}}
{{repo.relatedPackages}}
{{meta.blogPosts}}
{{pkg.install}}
{{pkg.size}}
{{pkg.deps}}
{{repo.examples}}
{{pkg.docs}}
TODO
Currently, documentation only exists in the form of small examples and various doc strings (incomplete). I'm working to alleviate this situation ASAP... In that respect, PRs are welcome as well!
import { $compile } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
import { cycle, map } from "@thi.ng/transducers";
// reactive value
const bg = reactive("gray");
// color options (infinite iterable)
const colors = cycle(["magenta", "yellow", "cyan"]);
// event handler
const nextColor = () => bg.next(<string>colors.next().value);
// define component tree in hiccup syntax, compile & mount component.
// each time `bg` value changes, only subscribed bits will be updated
// i.e. title, the button's `style.background` and its label
// Note: instead of direct hiccup syntax, you could also use the
// element functions provided by https://thi.ng/hiccup-html
$compile([
"div",
{},
// transformed color as title (aka derived view)
["h1", {}, bg.map((col) => `Hello, ${col}!`)],
[
// tag with Emmet-style ID & classes
"button#foo.w4.pa3.bn",
{
// reactive CSS background property
style: { background: bg },
onclick: nextColor,
},
// reactive button label
bg,
],
]).mount(document.body);
See $list
and
$klist
docs for
further information...
import { $klist } from "@thi.ng/rdom";
import { reactive } from "@thi.ng/rstream";
const items = reactive([
{ id: "a", val: 1 },
{ id: "b", val: 2 },
{ id: "c", val: 3 },
]);
$klist(
// reactive data source (any rstream subscribable)
items,
// outer list element & attribs
"ul",
{ class: "list red" },
// list item component constructor
(x) => ["li", {}, x.id, ` (${x.val})`],
// key function (includes)
(x) => `${x.id}-${x.val}`
).mount(document.body);
// update list:
// - item a will be removed
// - item b is unchanged
// - item d will be newly inserted
// - item c will be updated (due to new value)
setTimeout(
() => {
items.next([
{ id: "b", val: 2 },
{ id: "d", val: 4 },
{ id: "c", val: 30 },
]);
},
1000
);