thi-ng/umbrella

View on GitHub
packages/hdom/src/start.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { derefContext } from "@thi.ng/hiccup/deref";
import type { HDOMImplementation, HDOMOpts } from "./api.js";
import { DEFAULT_IMPL } from "./default.js";
import { resolveRoot } from "./resolve.js";

/**
 * Takes an hiccup tree (array, function or component object w/ life cycle
 * methods) and an optional object of DOM update options. 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 `ctx`
 * option can be used for passing arbitrary config data or state down into the
 * hiccup component tree. Any embedded component function in the tree will
 * receive this context object (shallow copy) as first argument, as will life
 * cycle methods in component objects. If the `autoDerefKeys` option is given,
 * attempts to auto-expand/deref the given keys in the user supplied context
 * object (`ctx` option) prior to *each* tree normalization. All of these values
 * should implement the
 * [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html) interface
 * (e.g. atoms, cursors, views, rstreams etc.). This feature can be used to
 * define dynamic contexts linked to the main app state, e.g. using derived
 * views provided by [`thi.ng/atom`](https://thi.ng/atom).
 *
 * **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.
 *
 * **Important:** Unless the `hydrate` option is enabled, 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. Likewise,
 * if `hydrate` is enabled, it is assumed that an equivalent DOM (minus
 * listeners) already exists (i.e. generated via SSR) when `start()` is called.
 * Any other discrepancies between the pre-existing DOM and the hdom trees will
 * cause undefined behavior.
 *
 * Returns a function, which when called, immediately cancels the update loop.
 *
 * @param tree - hiccup DOM tree
 * @param opts - options
 * @param impl - hdom target implementation
 */
export const start = (
    tree: any,
    opts: Partial<HDOMOpts> = {},
    impl: HDOMImplementation<any> = DEFAULT_IMPL
) => {
    const _opts = { root: "app", ...opts };
    let prev: any[] = [];
    let isActive = true;
    const root = resolveRoot(_opts.root, impl);
    const update = () => {
        if (isActive) {
            _opts.ctx = derefContext(opts.ctx, _opts.autoDerefKeys);
            const curr = impl.normalizeTree(_opts, tree);
            if (curr != null) {
                if (_opts.hydrate) {
                    impl.hydrateTree(_opts, root, curr);
                    _opts.hydrate = false;
                } else {
                    impl.diffTree(_opts, root, prev, curr);
                }
                prev = curr;
            }
            // check again in case one of the components called cancel
            isActive && requestAnimationFrame(update);
        }
    };
    requestAnimationFrame(update);
    return () => (isActive = false);
};