UNPKG

@tko/utils.jsx

Version:

TKO JSX Rendering

320 lines (319 loc) 10.8 kB
// @tko/utils.jsx 🥊 4.0.0-beta1.3 ESM import { LifeCycle } from "@tko/lifecycle"; import { safeStringify, isThenable } from "@tko/utils"; import { applyBindings, contextFor } from "@tko/bind"; import { isObservable, unwrap, observable } from "@tko/observable"; import { isComputed } from "@tko/computed"; import { NativeProvider, NATIVE_BINDINGS } from "@tko/provider.native"; import { queueCleanNode } from "./jsxClean"; export const ORIGINAL_JSX_SYM = Symbol("Knockout - Original JSX"); const NAMESPACES = { svg: "http://www.w3.org/2000/svg", html: "http://www.w3.org/1999/xhtml", xml: "http://www.w3.org/XML/1998/namespace", xlink: "http://www.w3.org/1999/xlink", xmlns: "http://www.w3.org/2000/xmlns/" }; function isIterable(v) { return v && typeof v[Symbol.iterator] === "function"; } export class JsxObserver extends LifeCycle { constructor(jsxOrObservable, parentNode, insertBefore = null, xmlns, noInitialBinding) { super(); const parentNodeIsComment = parentNode.nodeType === 8; const parentNodeTarget = this.getParentTarget(parentNode); if (isObservable(jsxOrObservable)) { jsxOrObservable.extend({ trackArrayChanges: true }); this.subscribe(jsxOrObservable, this.observableArrayChange, "arrayChange"); if (!insertBefore) { const insertAt = parentNodeIsComment ? parentNode.nextSibling : null; insertBefore = this.createComment("O"); parentNodeTarget.insertBefore(insertBefore, insertAt); } else { this.adoptedInsertBefore = true; } } if (parentNodeIsComment && !insertBefore) { insertBefore = parentNode.nextSibling; this.adoptedInsertBefore = true; } this.anchorTo(insertBefore || parentNode); Object.assign(this, { insertBefore, noInitialBinding, parentNode, parentNodeTarget, xmlns, nodeArrayOrObservableAtIndex: [], subscriptionsForNode: /* @__PURE__ */ new Map() }); const jsx = unwrap(jsxOrObservable); const computed = isComputed(jsxOrObservable); if (computed || jsx !== null && jsx !== void 0) { this.observableArrayChange(this.createInitialAdditions(jsx)); } this.noInitialBinding = false; } getParentTarget(parentNode) { if ("content" in parentNode) { return parentNode.content; } if (parentNode.nodeType === 8) { return parentNode.parentNode; } return parentNode; } remove() { this.dispose(); } dispose() { super.dispose(); const ib = this.insertBefore; const insertBeforeIsChild = ib && this.parentNodeTarget === ib.parentNode; if (insertBeforeIsChild && !this.adoptedInsertBefore) { this.parentNodeTarget.removeChild(ib); } this.removeAllPriorNodes(); Object.assign(this, { parentNode: null, parentNodeTarget: null, insertBefore: null, nodeArrayOrObservableAtIndex: [] }); for (const subscriptions of this.subscriptionsForNode.values()) { subscriptions.forEach((s) => s.dispose()); } this.subscriptionsForNode.clear(); } createInitialAdditions(possibleIterable) { const status = "added"; if (typeof possibleIteratable === "object" && posibleIterable !== null && Symbol.iterator in possibleIterable) { possibleIterable = [...possibleIterable]; } return Array.isArray(possibleIterable) ? possibleIterable.map((value, index) => ({ index, status, value })) : [{ status, index: 0, value: possibleIterable }]; } observableArrayChange(changes) { let adds = []; let dels = []; for (const index in changes) { const change = changes[index]; if (change.status === "added") { adds.push([change.index, change.value]); } else { dels.unshift([change.index, change.value]); } } dels.forEach((change) => this.delChange(...change)); adds.forEach((change) => this.addChange(...change)); } addChange(index, jsx) { this.nodeArrayOrObservableAtIndex.splice(index, 0, this.injectNode(jsx, this.lastNodeFor(index))); } injectNode(jsx, nextNode) { let nodeArrayOrObservable; if (isObservable(jsx)) { const { parentNode, xmlns } = this; const observer = new JsxObserver(jsx, parentNode, nextNode, xmlns, this.noInitialBinding); nodeArrayOrObservable = [observer]; } else if (typeof jsx !== "string" && isIterable(jsx)) { nodeArrayOrObservable = []; for (const child of jsx) { nodeArrayOrObservable.unshift(this.injectNode(child, nextNode)); } } else { const $context = contextFor(this.parentNode); const isInsideTemplate = "content" in this.parentNode; const shouldApplyBindings = $context && !isInsideTemplate && !this.noInitialBinding; if (Array.isArray(jsx)) { nodeArrayOrObservable = jsx.map((j) => this.anyToNode(j)); } else { nodeArrayOrObservable = [this.anyToNode(jsx)]; } for (const node of nodeArrayOrObservable) { this.parentNodeTarget.insertBefore(node, nextNode); if (shouldApplyBindings && this.canApplyBindings(node)) { applyBindings($context, node); } } } return nodeArrayOrObservable; } canApplyBindings(node) { return node.nodeType === 1 || node.nodeType === 8; } delChange(index) { this.removeNodeArrayOrObservable(this.nodeArrayOrObservableAtIndex[index]); this.nodeArrayOrObservableAtIndex.splice(index, 1); } getSubscriptionsForNode(node) { if (!this.subscriptionsForNode.has(node)) { const subscriptions = []; this.subscriptionsForNode.set(node, subscriptions); return subscriptions; } return this.subscriptionsForNode.get(node); } isJsx(jsx) { return typeof jsx.elementName === "string" && "children" in jsx && "attributes" in jsx; } anyToNode(any) { if (isThenable(any)) { return this.futureJsxNode(any); } switch (typeof any) { case "object": if (any instanceof Error) { return this.createComment(any.toString()); } if (any === null) { return this.createComment(String(any)); } if (any instanceof Node) { return this.cloneJSXorMoveNode(any); } if (Symbol.iterator in any) { return any; } break; case "function": return this.anyToNode(any()); case "undefined": case "symbol": return this.createComment(String(any)); case "string": return this.createTextNode(any); case "boolean": case "number": case "bigint": default: return this.createTextNode(String(any)); } return this.isJsx(any) ? this.jsxToNode(any) : this.createComment(safeStringify(any)); } createComment(string) { const node = document.createComment(string); node[NATIVE_BINDINGS] = true; return node; } createTextNode(string) { const node = document.createTextNode(string); node[NATIVE_BINDINGS] = true; return node; } cloneJSXorMoveNode(node) { return ORIGINAL_JSX_SYM in node ? this.jsxToNode(node[ORIGINAL_JSX_SYM]) : node; } jsxToNode(jsx) { const xmlns = jsx.attributes.xmlns || NAMESPACES[jsx.elementName] || this.xmlns; const node = document.createElementNS(xmlns || NAMESPACES.html, jsx.elementName); node[ORIGINAL_JSX_SYM] = jsx; if (isObservable(jsx.attributes)) { const subscriptions = this.getSubscriptionsForNode(node); subscriptions.push(jsx.attributes.subscribe((attrs) => { this.updateAttributes(node, unwrap(attrs)); })); } this.updateAttributes(node, unwrap(jsx.attributes)); this.addDisposable(new JsxObserver(jsx.children, node, null, xmlns, this.noInitialBinding)); return node; } futureJsxNode(promise) { const obs = observable(); promise.then(obs).catch((e) => obs(e instanceof Error ? e : Error(e))); const jo = new JsxObserver(obs, this.parentNode, null, this.xmlns, this.noInitialBinding); this.addDisposable(jo); return jo.insertBefore; } updateAttributes(node, attributes) { const subscriptions = this.getSubscriptionsForNode(node); const toRemove = new Set([...node.attributes].map((n) => n.name)); for (const [name, value] of Object.entries(attributes || {})) { toRemove.delete(name); if (isObservable(value)) { subscriptions.push(value.subscribe((attr) => this.setNodeAttribute(node, name, value))); } this.setNodeAttribute(node, name, value); } for (const name of toRemove) { this.setNodeAttribute(node, name, void 0); } } getNamespaceOfAttribute(attr) { const [prefix, ...unqualifiedName] = attr.split(":"); if (prefix === "xmlns" || unqualifiedName.length && NAMESPACES[prefix]) { return NAMESPACES[prefix]; } return null; } setNodeAttribute(node, name, valueOrObservable) { const value = unwrap(valueOrObservable); NativeProvider.addValueToNode(node, name, valueOrObservable); if (value === void 0) { node.removeAttributeNS(null, name); } else if (isThenable(valueOrObservable)) { Promise.resolve(valueOrObservable).then((v) => this.setNodeAttribute(node, name, v)); } else { const ns = this.getNamespaceOfAttribute(name); node.setAttributeNS(ns, name, String(value)); } } lastNodeFor(index) { const nodesAtIndex = this.nodeArrayOrObservableAtIndex[index] || []; const [lastNodeOfPrior] = nodesAtIndex.slice(-1); const insertBefore = lastNodeOfPrior instanceof JsxObserver ? lastNodeOfPrior.insertBefore : lastNodeOfPrior || this.insertBefore; if (insertBefore) { return insertBefore.parentNode ? insertBefore : null; } return null; } removeAllPriorNodes() { const { nodeArrayOrObservableAtIndex } = this; while (nodeArrayOrObservableAtIndex.length) { this.removeNodeArrayOrObservable(nodeArrayOrObservableAtIndex.pop()); } } removeNodeArrayOrObservable(nodeArrayOrObservable) { for (const nodeOrObservable of nodeArrayOrObservable) { if (nodeOrObservable instanceof JsxObserver) { nodeOrObservable.dispose(); continue; } const node = nodeOrObservable; delete node[ORIGINAL_JSX_SYM]; this.detachAndDispose(node); const subscriptions = this.subscriptionsForNode.get(node); if (subscriptions) { subscriptions.forEach((s) => s.dispose()); this.subscriptionsForNode.delete(node); } } } detachAndDispose(node) { if (isIterable(node)) { for (const child of node) { this.detachAndDispose(child); } } else { node.remove(); } queueCleanNode(node); } } export default JsxObserver;