UNPKG

uhtml

Version:

A minimalistic library to create fast and reactive Web pages

266 lines (248 loc) 8.2 kB
//@ts-nocheck import DEBUG from '../debug.js'; import errors from '../errors.js'; import resolve from './resolve.js'; import { children } from './ish.js'; import { effect } from './signals.js'; import { isArray } from '../utils.js'; import { PersistentFragment, diffFragment, nodes } from './persistent-fragment.js'; import { ARRAY, COMMENT, COMPONENT, EVENT, KEY, REF, SIGNAL, ref } from './update.js'; import { _get as getDirect, _set as setDirect } from './direct.js'; import { Signal, _get as getSignal, _set as setSignal } from './signals.js'; /** * @param {Hole} hole * @returns */ export const dom = hole => diffFragment(hole.n ? hole.update(hole) : hole.valueOf(false), 1); const holed = (prev, current) => { const changes = [], h = prev.length, l = current.length; for (let c, p, j = 0, i = 0; i < l; i++) { c = current[i]; changes[i] = j < h && (p = prev[j++]).t === c.t ? (current[i] = p).update(c) : c.valueOf(false); } return changes; }; /** * @param {Hole} hole * @param {unknown} value * @returns {Node} */ const keyed = (hole, value) => /** @type {import('./keyed.js').Keyed} */(hole.t[2]).get(value)?.update(hole) ?? hole.valueOf(false); /** * * @param {Function} Component * @param {Object} obj * @param {unknown[]} signals * @returns {Hole} */ const component = (Component, obj, signals) => { const signal = getSignal(); const length = signals.length; let i = 0; setSignal(/** @param {unknown} value */ value => i < length ? signals[i++] : (signals[i++] = signal(value))); const wasDirect = getDirect(); if (wasDirect) setDirect(!wasDirect); try { return Component(obj, global); } finally { if (wasDirect) setDirect(wasDirect); setSignal(signal); } }; /** * @param {Hole} hole * @param {Hole} value * @returns {Hole} */ const getHole = (hole, value) => { if (hole.t === value.t) { hole.update(value); } else { hole.n.replaceWith(dom(value)); hole = value; } return hole; }; const createEffect = (node, value, obj) => { let signals = [], entry = [COMPONENT, null, obj], bootstrap = true, hole; effect(() => { if (bootstrap) { bootstrap = false; hole = component(value, obj, signals); if (!signals.length) signals = children; if (hole) { node.replaceWith(dom(hole)); entry[1] = hole; } else node.remove(); } else { const result = component(value, obj, signals); if (hole) { if (DEBUG && !(result instanceof Hole)) throw errors.invalid_component(value); if (getHole(hole, /** @type {Hole} */(result)) === result) entry[2] = (hole = result); } } }); return entry; }; const updateRefs = refs => { for (const node of refs) { const value = node[ref]; if (typeof value === 'function') value(node); else if (value instanceof Signal) value.value = node; else if (value) value.current = node; } }; const props = Symbol(); const global = {}; export class Hole { /** * @param {[DocumentFragment, unknown[], import('./keyed.js').Keyed?]} template * @param {unknown[]} values */ constructor(template, values) { this.t = template; this.v = values; this.n = null; this.k = -1; } /** * @param {boolean} [direct] * @returns {Node} */ valueOf(direct = getDirect()) { const [fragment, updates, keys] = this.t; const root = document.importNode(fragment, true); const values = this.v; let length = values.length; let changes = children; let node, prev, refs; if (DEBUG && length !== updates.length) throw errors.invalid_interpolation(this.t[3], values); if (0 < length) { changes = updates.slice(0); while (length--) { const [path, update, type] = updates[length]; const value = values[length]; if (prev !== path) { node = resolve(root, path); prev = path; if (DEBUG && !node) throw errors.invalid_path(this.t[3], path); } if (type & COMPONENT) { const obj = node[props] || (node[props] = {}); if (type === COMPONENT) { for (const { name, value } of node.attributes) obj[name] ??= value; obj.children ??= [...node.content.childNodes]; changes[length] = createEffect(node, value, obj); } else { update(obj, value); changes[length] = [type, update, obj]; } } else { let commit = true; if (DEBUG && (type & ARRAY) && !isArray(value)) throw errors.invalid_interpolation(this.t[3], value); if (!direct && (type & COMMENT) && !(type & SIGNAL)) { if (type & ARRAY) { commit = false; if (value.length) update(node, value[0] instanceof Hole ? holed(children, value) : value); } else if (value instanceof Hole) { commit = false; update(node, dom(value)); } } if (commit) { if (type === KEY) { if (DEBUG && !keys) throw errors.invalid_key(value); this.k = length; } else { if (type === REF) (refs ??= new Set).add(node); update(node, value); } } changes[length] = [type, update, value, node]; if (direct && (type & COMMENT)) node.remove(); } } if (refs) updateRefs(refs); } const { childNodes } = root; const size = childNodes.length; const n = size === 1 ? childNodes[0] : (size ? PersistentFragment(root) : root); this.v = changes; this.n = n; if (-1 < this.k) keys.set(changes[this.k][2], n, this); return n; } /** * @param {Hole} hole * @returns {Node} */ update(hole) { const key = this.k; const changes = this.v; const values = hole.v; if (-1 < key && changes[key][2] !== values[key]) return keyed(hole, values[key]); let { length } = changes; while (length--) { const entry = changes[length]; const [type, update, prev] = entry; if (type === KEY) continue; let value = values[length]; if (type & COMPONENT) { if (type === COMPONENT) { if (DEBUG && typeof value !== 'function') throw errors.invalid_component(value); const result = value(prev, global); if (update) { if (DEBUG && !(result instanceof Hole)) throw errors.invalid_component(value); if (getHole(update, /** @type {Hole} */(result)) === result) entry[2] = result; } } else update(prev, value); } else { let change = value; if (type & ARRAY) { if (DEBUG && !isArray(value)) throw errors.invalid_interpolation([], value); if (type & COMMENT) { // TODO: a smarter differ that does not require 2 loops if (value.length) { if (value[0] instanceof Hole) { if (DEBUG && prev.length && !(prev[0] instanceof Hole)) throw errors.invalid_interpolation([], value[0]); change = holed(prev, value); } } } else if ((type & EVENT) && (value[0] === prev[0])) continue; } else if (type & COMMENT) { if (type & SIGNAL) { if (value === prev) { update(entry[3], change); continue; } } else if (prev instanceof Hole) { if (DEBUG && !(value instanceof Hole)) throw errors.invalid_interpolation([], value); value = getHole(prev, /** @type {Hole} */(value)); change = value.n; } } if (value !== prev) { entry[2] = value; update(entry[3], change); } } } return /** @type {Node} */(this.n); } }