From 3fd5f8edac2ca8b8dd92018d939734cf23b45f5c Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Wed, 30 Nov 2022 13:16:21 +0100 Subject: [PATCH] feat(rdom): add DOM comment support (#367), other refactorings - add $comment(), isComment() - add Component.$comment() syntax sugar - add comment check/branch in $tree() - update args for $addChild(), $remove(), $moveTo() - update $text(), $html() to support SVG elements - add doc strings --- packages/rdom/src/checks.ts | 18 ++++++ packages/rdom/src/component.ts | 102 +++++++++++++++++++++++++++++++-- packages/rdom/src/dom.ts | 94 +++++++++++++++++++++++++----- 3 files changed, 195 insertions(+), 19 deletions(-) diff --git a/packages/rdom/src/checks.ts b/packages/rdom/src/checks.ts index ad2dfc6991..b2152e32d9 100644 --- a/packages/rdom/src/checks.ts +++ b/packages/rdom/src/checks.ts @@ -1,5 +1,23 @@ import { implementsFunction } from "@thi.ng/checks/implements-function"; +import { COMMENT } from "@thi.ng/hiccup/api"; import type { IComponent } from "./api.js"; +/** + * Returns true if given hiccup component describes a comment node. I.e. is of + * the form `[COMMENT, "foo"...]`. + * + * @remarks + * See thi.ng/hiccup docs for reference: + * - https://docs.thi.ng/umbrella/hiccup/functions/serialize.html + * + * @param tree + */ +export const isComment = (tree: any[]) => tree[0] === COMMENT; + +/** + * Returns true, if given value has a {@link IComponent.mount} function. + * + * @param x + */ export const isComponent = (x: any): x is IComponent => implementsFunction(x, "mount"); diff --git a/packages/rdom/src/component.ts b/packages/rdom/src/component.ts index e6b02d702c..26f9af220d 100644 --- a/packages/rdom/src/component.ts +++ b/packages/rdom/src/component.ts @@ -4,6 +4,7 @@ import { $compile } from "./compile.js"; import { $attribs, $clear, + $comment, $el, $html, $moveTo, @@ -35,6 +36,16 @@ export abstract class Component implements IComponent { // @ts-ignore args update(state?: T) {} + /** + * Syntax sugar for {@link $el}, using this component's + * {@link IComponent.el} as default `parent`. + * + * @param tag + * @param attribs + * @param body + * @param parent + * @param idx + */ $el( tag: string, attribs?: any, @@ -45,38 +56,121 @@ export abstract class Component implements IComponent { return $el(tag, attribs, body, parent, idx); } + /** + * Syntax sugar for {@link $comment}, creates a new comment DOM node using + * this component's {@link IComponent.el} as default `parent`. + * + * @param body + * @param parent + * @param idx + */ + $comment(body: string | string[], parent = this.el, idx?: NumOrElement) { + return $comment(body, parent, idx); + } + + /** + * Syntax sugar for {@link $clear}, using this component's + * {@link IComponent.el} as default element to clear. + * + * @param el + */ $clear(el = this.el!) { return $clear(el); } + /** + * Same as {@link $compile}. + * + * @param tree + */ $compile(tree: any) { return $compile(tree); } + /** + * Same as {@link $tree}. + * + * @param tree + * @param root + * @param index + */ $tree(tree: any, root = this.el!, index?: NumOrElement) { return $tree(tree, root, index); } - $text(body: any) { - this.el && $text(this.el, body); + /** + * Syntax sugar for {@link $text}, using this component's + * {@link IComponent.el} as default element to edit. + * + * @remarks + * If using the default element, assumes `this.el` is an existing + * `HTMLElement`. + * + * @param body + * @param el + */ + $text(body: any, el: HTMLElement = this.el!) { + $text(el, body); } - $html(body: MaybeDeref) { - this.el && $html(this.el, body); + /** + * Syntax sugar for {@link $html}, using this component's + * {@link IComponent.el} as default element to edit. + * + * @remarks + * If using the default element, assumes `this.el` is an existing + * `HTMLElement` or `SVGElement`. + * + * @param body + * @param el + */ + $html( + body: MaybeDeref, + el: HTMLElement | SVGElement = this.el! + ) { + $html(el, body); } + /** + * Syntax sugar for {@link $attribs}, using this component's + * {@link IComponent.el} as default element to edit. + * + * @param attribs + * @param el + */ $attribs(attribs: any, el = this.el!) { $attribs(el, attribs); } + /** + * Syntax sugar for {@link $style}, using this component's + * {@link IComponent.el} as default element to edit. + * + * @param rules + * @param el + */ $style(rules: any, el = this.el!) { $style(el, rules); } + /** + * Syntax sugar for {@link $remove}, using this component's + * {@link IComponent.el} as default element to remove. + * + * @param el + */ $remove(el = this.el!) { $remove(el); } + /** + * Syntax sugar for {@link $moveTo}, using this component's + * {@link IComponent.el} as default element to migrate. + * + * @param newParent + * @param el + * @param idx + */ $moveTo(newParent: Element, el = this.el!, idx?: NumOrElement) { $moveTo(newParent, el, idx); } diff --git a/packages/rdom/src/dom.ts b/packages/rdom/src/dom.ts index 8460a5232c..bcfbcfde42 100644 --- a/packages/rdom/src/dom.ts +++ b/packages/rdom/src/dom.ts @@ -18,25 +18,29 @@ import { mergeClasses, mergeEmmetAttribs } from "@thi.ng/hiccup/attribs"; import { formatPrefixes } from "@thi.ng/hiccup/prefix"; import { XML_SVG, XML_XLINK, XML_XMLNS } from "@thi.ng/prefixes/xml"; import type { NumOrElement } from "./api.js"; -import { isComponent } from "./checks.js"; +import { isComment, isComponent } from "./checks.js"; /** - * hdom-style DOM tree creation from hiccup format. Returns DOM element - * of `tree` root. See {@link $el} for further details. + * hdom-style DOM tree creation from hiccup format. Returns DOM element of + * `tree` root. See {@link $el} for further details. * * @remarks * Supports elements given in these forms: * * - {@link IComponent} instance - * - {@link IDeref} instance (must resolve to another supported type in - * this list) + * - {@link IDeref} instance (must resolve to another supported type in this + * list) * - `["div#id.class", {...attribs}, ...children]` + * - `[COMMENT, "foo", "bar"...]` (DOM comment node) * - `[IComponent, ...mountargs]` * - `[function, ...args]` * - ES6 iterable of the above (for child values only!) * - * Any other values will be cast to strings and added as spans to - * current `parent`. + * Any other values will be cast to strings and added as spans to current + * `parent`. + * + * Note: `COMMENT` is defined as constant in thi.ng/hiccup package. Also see + * {@link $comment} function to create comments directly. * * @param tree - * @param parent - @@ -48,7 +52,9 @@ export const $tree = async ( idx: NumOrElement = -1 ): Promise => isArray(tree) - ? $treeElem(tree, parent, idx) + ? isComment(tree) + ? $comment(tree.slice(1), parent, idx) + : $treeElem(tree, parent, idx) : isComponent(tree) ? tree.mount(parent, idx) : isDeref(tree) @@ -144,9 +150,47 @@ export const $el = ( return el; }; +/** + * Similar to {@link $el}, but creates a new comment DOM node using provided + * body. If `parent` is given, the comment will be attached or inserted as child + * at `idx`. Returns comment node. + * + * @remarks + * See thi.ng/hiccup docs for reference: + * - https://docs.thi.ng/umbrella/hiccup/functions/serialize.html + * + * @param body + * @param parent + * @param idx + */ +export const $comment = ( + body: string | string[], + parent?: Element, + idx: NumOrElement = -1 +) => { + const comment = document.createComment( + isString(body) + ? body + : body.length < 2 + ? body[0] || "" + : ["", ...body, ""].join("\n") + ); + parent && $addChild(parent, comment, idx); + return comment; +}; + +/** + * Appends or inserts `child` as child element of `parent`. The default `idx` of + * -1 means the child will be appended, else uses `parent.insertBefore()` to + * insert at given index. + * + * @param parent + * @param child + * @param idx + */ export const $addChild = ( parent: Element, - child: Element, + child: Element | Comment, idx: NumOrElement = -1 ) => { isNumber(idx) @@ -156,17 +200,35 @@ export const $addChild = ( : parent.insertBefore(child, idx); }; -export const $remove = (el: Element) => el.remove(); +/** + * Removes given element or comment from the DOM. + * + * @param el + */ +export const $remove = (el: Element | Comment) => el.remove(); +/** + * Migrates given element to `newParent`, following the same append or insertion + * logic as {@link $addChild}. + * + * @param newParent + * @param el + * @param idx + */ export const $moveTo = ( newParent: Element, - el: Element, + el: Element | Comment, idx: NumOrElement = -1 ) => { $remove(el); $addChild(newParent, el, idx); }; +/** + * Removes all content from given element. + * + * @param el + */ export const $clear = (el: Element) => ((el.innerHTML = ""), el); /** @@ -177,12 +239,12 @@ export const $clear = (el: Element) => ((el.innerHTML = ""), el); * @param el - * @param body - */ -export const $text = (el: HTMLElement, body: any) => { +export const $text = (el: HTMLElement | SVGElement, body: any) => { body = String(deref(body)); if (el.namespaceURI === XML_SVG) { $clear(el).appendChild(document.createTextNode(body)); } else { - el.innerText = body; + (el).innerText = body; } }; @@ -193,8 +255,10 @@ export const $text = (el: HTMLElement, body: any) => { * @param el - * @param body - */ - -export const $html = (el: HTMLElement, body: MaybeDeref) => { +export const $html = ( + el: HTMLElement | SVGElement, + body: MaybeDeref +) => { el.innerHTML = String(deref(body)); };