@seahax/elemental
Advanced tools
+2
-22
| type Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'; | ||
| type ChildValue = Node | HtmlDeferredElement | string | number | bigint | false | null | undefined; | ||
| type ChildValue = Node | string | number | bigint | false | null | undefined; | ||
| type AttrValue = string | number | bigint | boolean | null | undefined; | ||
@@ -12,27 +12,7 @@ type TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap; | ||
| export type HtmlConfig<TElement> = ElementAttrs<TElement> & ElementProps<ElementType<TElement>>; | ||
| export type HtmlConfigWithKey<TElement> = { | ||
| readonly [DATA_KEY]: string; | ||
| } & HtmlConfig<TElement>; | ||
| export interface HtmlDeferredElement<TElement extends HTMLElement = HTMLElement> { | ||
| readonly tag: string; | ||
| readonly config: { | ||
| readonly [DATA_KEY]: string; | ||
| } & HtmlConfig<TElement>; | ||
| readonly children: readonly ChildValue[] | undefined; | ||
| toElement: () => TElement; | ||
| } | ||
| declare const DATA_KEY = "data-key"; | ||
| /** | ||
| * Create a deferred HTML element with a key. | ||
| * | ||
| * Final element creation is deferred until the element is used as a child, | ||
| * when it can be determined if a new element should be created, or an existing | ||
| * element with the same key and tag-name should be updated. | ||
| */ | ||
| export declare function html<const TElement extends keyof TagMap | (string & {}) | CustomElementConstructor>(el: TElement, config: HtmlConfigWithKey<TElement>, children?: readonly ChildValue[]): HtmlDeferredElement<ElementType<TElement>>; | ||
| /** | ||
| * Create or update an HTML element. | ||
| */ | ||
| export declare function html<TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot>(el: TElement, attrs?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>; | ||
| export declare function html<TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot>(el: TElement, config?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>; | ||
| export declare function html<TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot>(el: TElement, children?: readonly ChildValue[]): ElementType<TElement>; | ||
| export {}; |
+21
-33
| //#region src/html.ts | ||
| var e = "data-key"; | ||
| function t(n, r = {}, i = []) { | ||
| if (typeof n == "function") { | ||
| let e = customElements.getName(n); | ||
| e ?? (e = `ce-${crypto.randomUUID()}`, customElements.define(e, n)), n = e; | ||
| function e(e, t = {}, n) { | ||
| if (typeof e == "function") { | ||
| let t = customElements.getName(e); | ||
| t ?? (t = `ce-${crypto.randomUUID()}`, customElements.define(t, e)), e = t; | ||
| } | ||
| let a; | ||
| if ([a, i] = Array.isArray(r) ? [{}, r] : [r, i], typeof n == "string") return e in a ? { | ||
| tag: n, | ||
| config: a, | ||
| children: i, | ||
| toElement: () => t(document.createElement(n), a, i) | ||
| } : t(document.createElement(n), a, i); | ||
| if ("setAttribute" in n) { | ||
| for (let [e, t] of Object.entries(a)) if (!(t === void 0 || e.startsWith(":"))) if (t === null || t === !1) n.hasAttribute(e) && n.removeAttribute(e); | ||
| typeof e == "string" && (e = document.createElement(e)); | ||
| let r; | ||
| if ([r, n] = Array.isArray(t) ? [{}, t] : [t, n], "setAttribute" in e) { | ||
| for (let [t, n] of Object.entries(r)) if (!(n === void 0 || t.startsWith(":"))) if (n === null || n === !1) e.hasAttribute(t) && e.removeAttribute(t); | ||
| else { | ||
| let r = t == 1 ? "" : String(t); | ||
| if (n.getAttribute(e) !== r) try { | ||
| n.setAttribute(e, r); | ||
| let r = n == 1 ? "" : String(n); | ||
| if (e.getAttribute(t) !== r) try { | ||
| e.setAttribute(t, r); | ||
| } catch (e) { | ||
@@ -26,22 +20,16 @@ console.warn(e); | ||
| } | ||
| for (let [e, t] of Object.entries(a)) { | ||
| if (t === void 0 || !e.startsWith(":")) continue; | ||
| let r = e.slice(1); | ||
| Reflect.get(n, r) !== t && Reflect.set(n, r, t); | ||
| for (let [t, n] of Object.entries(r)) { | ||
| if (n === void 0 || !t.startsWith(":")) continue; | ||
| let r = t.slice(1); | ||
| Reflect.get(e, r) !== n && Reflect.set(e, r, n); | ||
| } | ||
| let o, s = document.activeElement; | ||
| return n.replaceChildren(...i.filter((e) => e != null && e !== !1).map((r) => { | ||
| if (typeof r != "object") return document.createTextNode(String(r)); | ||
| if (r instanceof Node) return r; | ||
| o ??= new Map([...n.children].flatMap((t) => { | ||
| let n = t.getAttribute(e); | ||
| return n == null ? [] : [[n, t]]; | ||
| })); | ||
| let i = r.config[e], a = o.get(i); | ||
| return a && a.tagName.toLowerCase() === r.tag ? (o.delete(i), t(a, r.config, r.children)) : r.toElement(); | ||
| })), s?.isConnected && document.activeElement !== s && s.focus(), n; | ||
| if (n) { | ||
| let t = n.filter((e) => e != null && e !== !1).map((e) => typeof e == "object" ? e : document.createTextNode(String(e))); | ||
| e.replaceChildren(...t); | ||
| } | ||
| return e; | ||
| } | ||
| //#endregion | ||
| export { t as html }; | ||
| export { e as html }; | ||
| //# sourceMappingURL=html.js.map |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["// prettier-ignore\ntype Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\ntype ChildValue = Node | HtmlDeferredElement | string | number | bigint | false | null | undefined;\ntype AttrValue = string | number | bigint | boolean | null | undefined;\ntype TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;\n\ntype IfEquals<T, U, Y = unknown, N = never> =\n (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N;\n\ntype ElementType<TElement> = TElement extends string\n ? TElement extends keyof TagMap\n ? TagMap[TElement]\n : HTMLElement\n : TElement extends CustomElementConstructor\n ? InstanceType<TElement>\n : TElement extends Element | Document | ShadowRoot\n ? TElement\n : never;\n\ntype ElementProps<TElement> = {\n readonly [P in keyof TElement & string as TElement[P] extends ((...args: any[]) => any) | null | undefined\n ? never\n : IfEquals<Record<P, TElement[P]>, Pick<TElement, P>, `:${P}`, never>]?: TElement[P];\n};\n\ntype ElementAttrs<TElement> = TElement extends string | CustomElementConstructor | Element\n ? Readonly<Record<`${Alpha}${string}`, AttrValue>>\n : {};\n\nexport type HtmlConfig<TElement> = ElementAttrs<TElement> & ElementProps<ElementType<TElement>>;\nexport type HtmlConfigWithKey<TElement> = { readonly [DATA_KEY]: string } & HtmlConfig<TElement>;\n\nexport interface HtmlDeferredElement<TElement extends HTMLElement = HTMLElement> {\n readonly tag: string;\n readonly config: { readonly [DATA_KEY]: string } & HtmlConfig<TElement>;\n readonly children: readonly ChildValue[] | undefined;\n toElement: () => TElement;\n}\n\nconst DATA_KEY = 'data-key';\n\n/**\n * Create a deferred HTML element with a key.\n *\n * Final element creation is deferred until the element is used as a child,\n * when it can be determined if a new element should be created, or an existing\n * element with the same key and tag-name should be updated.\n */\nexport function html<const TElement extends keyof TagMap | (string & {}) | CustomElementConstructor>(\n el: TElement,\n config: HtmlConfigWithKey<TElement>,\n children?: readonly ChildValue[],\n): HtmlDeferredElement<ElementType<TElement>>;\n\n/**\n * Create or update an HTML element.\n */\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, attrs?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>;\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, children?: readonly ChildValue[]): ElementType<TElement>;\n\nexport function html(\n el: string | CustomElementConstructor | Element | Document | ShadowRoot,\n attrsOrChildren: readonly ChildValue[] | Readonly<Record<string, any>> = {},\n children: readonly ChildValue[] = [],\n): Node | HtmlDeferredElement {\n if (typeof el === 'function') {\n let name = customElements.getName(el);\n\n if (name == null) {\n name = `ce-${crypto.randomUUID()}`;\n customElements.define(name, el);\n }\n\n el = name;\n }\n\n let attrs: Readonly<Record<string, any>>;\n [attrs, children] = Array.isArray(attrsOrChildren)\n ? [{}, attrsOrChildren as readonly ChildValue[]]\n : [attrsOrChildren as Readonly<Record<string, any>>, children];\n\n if (typeof el === 'string') {\n return DATA_KEY in attrs\n ? {\n tag: el,\n config: attrs as HtmlDeferredElement['config'],\n children,\n toElement: () => html(document.createElement(el), attrs, children),\n }\n : html(document.createElement(el), attrs, children);\n }\n\n if ('setAttribute' in el) {\n for (const [name, rawValue] of Object.entries(attrs)) {\n if (rawValue === undefined || name.startsWith(':')) continue;\n\n if (rawValue === null || rawValue === false) {\n if (el.hasAttribute(name)) el.removeAttribute(name);\n } else {\n const value = rawValue == true ? '' : String(rawValue);\n if (el.getAttribute(name) !== value) {\n try {\n el.setAttribute(name, value);\n } catch (error) {\n console.warn(error);\n }\n }\n }\n }\n }\n\n for (const [rawName, value] of Object.entries(attrs)) {\n if (value === undefined || !rawName.startsWith(':')) continue;\n const name = rawName.slice(1);\n if (Reflect.get(el, name) !== value) Reflect.set(el, name, value);\n }\n\n let keyElements: Map<string, Element> | undefined;\n const activeElement = document.activeElement;\n\n el.replaceChildren(\n ...children\n .filter((child) => child != null && child !== false)\n .map((child) => {\n if (typeof child !== 'object') return document.createTextNode(String(child));\n if (child instanceof Node) return child;\n\n keyElements ??= new Map(\n [...el.children].flatMap((child) => {\n const key = child.getAttribute(DATA_KEY);\n return key == null ? [] : [[key, child] as const];\n }),\n );\n\n const key = child.config[DATA_KEY];\n const reused = keyElements.get(key);\n\n if (reused && reused.tagName.toLowerCase() === child.tag) {\n keyElements.delete(key);\n return html(reused, child.config, child.children);\n }\n\n return child.toElement();\n }),\n );\n\n // Restore focus in case the current child contains focus.\n if (activeElement?.isConnected && document.activeElement !== activeElement) {\n (activeElement as HTMLElement).focus();\n }\n\n return el;\n}\n"],"mappings":";AAuCA,IAAM,IAAW;AAyBjB,SAAgB,EACd,GACA,IAAyE,EAAE,EAC3E,IAAkC,EAAE,EACR;AAC5B,KAAI,OAAO,KAAO,YAAY;EAC5B,IAAI,IAAO,eAAe,QAAQ,EAAG;AAOrC,EALI,MACF,IAAO,MAAM,OAAO,YAAY,IAChC,eAAe,OAAO,GAAM,EAAG,GAGjC,IAAK;;CAGP,IAAI;AAKJ,KAJA,CAAC,GAAO,KAAY,MAAM,QAAQ,EAAgB,GAC9C,CAAC,EAAE,EAAE,EAAyC,GAC9C,CAAC,GAAkD,EAAS,EAE5D,OAAO,KAAO,SAChB,QAAO,KAAY,IACf;EACE,KAAK;EACL,QAAQ;EACR;EACA,iBAAiB,EAAK,SAAS,cAAc,EAAG,EAAE,GAAO,EAAS;EACnE,GACD,EAAK,SAAS,cAAc,EAAG,EAAE,GAAO,EAAS;AAGvD,KAAI,kBAAkB,GACpB;OAAK,IAAM,CAAC,GAAM,MAAa,OAAO,QAAQ,EAAM,CAC9C,aAAa,KAAA,KAAa,EAAK,WAAW,IAAI,EAElD,KAAI,MAAa,QAAQ,MAAa,IAChC,EAAG,aAAa,EAAK,IAAE,EAAG,gBAAgB,EAAK;OAC9C;GACL,IAAM,IAAQ,KAAY,IAAO,KAAK,OAAO,EAAS;AACtD,OAAI,EAAG,aAAa,EAAK,KAAK,EAC5B,KAAI;AACF,MAAG,aAAa,GAAM,EAAM;YACrB,GAAO;AACd,YAAQ,KAAK,EAAM;;;;AAO7B,MAAK,IAAM,CAAC,GAAS,MAAU,OAAO,QAAQ,EAAM,EAAE;AACpD,MAAI,MAAU,KAAA,KAAa,CAAC,EAAQ,WAAW,IAAI,CAAE;EACrD,IAAM,IAAO,EAAQ,MAAM,EAAE;AAC7B,EAAI,QAAQ,IAAI,GAAI,EAAK,KAAK,KAAO,QAAQ,IAAI,GAAI,GAAM,EAAM;;CAGnE,IAAI,GACE,IAAgB,SAAS;AAiC/B,QA/BA,EAAG,gBACD,GAAG,EACA,QAAQ,MAAU,KAAS,QAAQ,MAAU,GAAM,CACnD,KAAK,MAAU;AACd,MAAI,OAAO,KAAU,SAAU,QAAO,SAAS,eAAe,OAAO,EAAM,CAAC;AAC5E,MAAI,aAAiB,KAAM,QAAO;AAElC,QAAgB,IAAI,IAClB,CAAC,GAAG,EAAG,SAAS,CAAC,SAAS,MAAU;GAClC,IAAM,IAAM,EAAM,aAAa,EAAS;AACxC,UAAO,KAAO,OAAO,EAAE,GAAG,CAAC,CAAC,GAAK,EAAM,CAAU;IACjD,CACH;EAED,IAAM,IAAM,EAAM,OAAO,IACnB,IAAS,EAAY,IAAI,EAAI;AAOnC,SALI,KAAU,EAAO,QAAQ,aAAa,KAAK,EAAM,OACnD,EAAY,OAAO,EAAI,EAChB,EAAK,GAAQ,EAAM,QAAQ,EAAM,SAAS,IAG5C,EAAM,WAAW;GACxB,CACL,EAGG,GAAe,eAAe,SAAS,kBAAkB,KAC1D,EAA8B,OAAO,EAGjC"} | ||
| {"version":3,"file":"html.js","names":[],"sources":["../src/html.ts"],"sourcesContent":["// prettier-ignore\ntype Alpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\ntype ChildValue = Node | string | number | bigint | false | null | undefined;\ntype AttrValue = string | number | bigint | boolean | null | undefined;\ntype TagMap = HTMLElementTagNameMap & HTMLElementDeprecatedTagNameMap;\n\ntype IfEquals<T, U, Y = unknown, N = never> =\n (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N;\n\ntype ElementType<TElement> = TElement extends string\n ? TElement extends keyof TagMap\n ? TagMap[TElement]\n : HTMLElement\n : TElement extends CustomElementConstructor\n ? InstanceType<TElement>\n : TElement extends Element | Document | ShadowRoot\n ? TElement\n : never;\n\ntype ElementProps<TElement> = {\n readonly [P in keyof TElement & string as TElement[P] extends ((...args: any[]) => any) | null | undefined\n ? never\n : IfEquals<Record<P, TElement[P]>, Pick<TElement, P>, `:${P}`, never>]?: TElement[P];\n};\n\ntype ElementAttrs<TElement> = TElement extends string | CustomElementConstructor | Element\n ? Readonly<Record<`${Alpha}${string}`, AttrValue>>\n : {};\n\nexport type HtmlConfig<TElement> = ElementAttrs<TElement> & ElementProps<ElementType<TElement>>;\n\n/**\n * Create or update an HTML element.\n */\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, config?: HtmlConfig<TElement>, children?: readonly ChildValue[]): ElementType<NoInfer<TElement>>;\nexport function html<\n TElement extends keyof TagMap | (string & {}) | CustomElementConstructor | Element | Document | ShadowRoot,\n>(el: TElement, children?: readonly ChildValue[]): ElementType<TElement>;\n\nexport function html(\n el: string | CustomElementConstructor | Element | Document | ShadowRoot,\n configOrChildren: readonly ChildValue[] | Readonly<Record<string, any>> = {},\n children?: readonly ChildValue[],\n): Node {\n if (typeof el === 'function') {\n let name = customElements.getName(el);\n\n if (name == null) {\n name = `ce-${crypto.randomUUID()}`;\n customElements.define(name, el);\n }\n\n el = name;\n }\n\n if (typeof el === 'string') el = document.createElement(el);\n\n let config: Readonly<Record<string, any>>;\n [config, children] = Array.isArray(configOrChildren)\n ? [{}, configOrChildren as readonly ChildValue[]]\n : [configOrChildren as Readonly<Record<string, any>>, children];\n\n if ('setAttribute' in el) {\n for (const [name, rawValue] of Object.entries(config)) {\n if (rawValue === undefined || name.startsWith(':')) continue;\n\n if (rawValue === null || rawValue === false) {\n if (el.hasAttribute(name)) el.removeAttribute(name);\n } else {\n const value = rawValue == true ? '' : String(rawValue);\n if (el.getAttribute(name) !== value) {\n try {\n el.setAttribute(name, value);\n } catch (error) {\n console.warn(error);\n }\n }\n }\n }\n }\n\n for (const [rawName, value] of Object.entries(config)) {\n if (value === undefined || !rawName.startsWith(':')) continue;\n const name = rawName.slice(1);\n if (Reflect.get(el, name) !== value) Reflect.set(el, name, value);\n }\n\n if (children) {\n const childNodes = children\n .filter((child) => child != null && child !== false)\n .map((child) => (typeof child !== 'object' ? document.createTextNode(String(child)) : child));\n\n el.replaceChildren(...childNodes);\n }\n\n return el;\n}\n"],"mappings":";AAyCA,SAAgB,EACd,GACA,IAA0E,EAAE,EAC5E,GACM;AACN,KAAI,OAAO,KAAO,YAAY;EAC5B,IAAI,IAAO,eAAe,QAAQ,EAAG;AAOrC,EALI,MACF,IAAO,MAAM,OAAO,YAAY,IAChC,eAAe,OAAO,GAAM,EAAG,GAGjC,IAAK;;AAGP,CAAI,OAAO,KAAO,aAAU,IAAK,SAAS,cAAc,EAAG;CAE3D,IAAI;AAKJ,KAJA,CAAC,GAAQ,KAAY,MAAM,QAAQ,EAAiB,GAChD,CAAC,EAAE,EAAE,EAA0C,GAC/C,CAAC,GAAmD,EAAS,EAE7D,kBAAkB,GACpB;OAAK,IAAM,CAAC,GAAM,MAAa,OAAO,QAAQ,EAAO,CAC/C,aAAa,KAAA,KAAa,EAAK,WAAW,IAAI,EAElD,KAAI,MAAa,QAAQ,MAAa,IAChC,EAAG,aAAa,EAAK,IAAE,EAAG,gBAAgB,EAAK;OAC9C;GACL,IAAM,IAAQ,KAAY,IAAO,KAAK,OAAO,EAAS;AACtD,OAAI,EAAG,aAAa,EAAK,KAAK,EAC5B,KAAI;AACF,MAAG,aAAa,GAAM,EAAM;YACrB,GAAO;AACd,YAAQ,KAAK,EAAM;;;;AAO7B,MAAK,IAAM,CAAC,GAAS,MAAU,OAAO,QAAQ,EAAO,EAAE;AACrD,MAAI,MAAU,KAAA,KAAa,CAAC,EAAQ,WAAW,IAAI,CAAE;EACrD,IAAM,IAAO,EAAQ,MAAM,EAAE;AAC7B,EAAI,QAAQ,IAAI,GAAI,EAAK,KAAK,KAAO,QAAQ,IAAI,GAAI,GAAM,EAAM;;AAGnE,KAAI,GAAU;EACZ,IAAM,IAAa,EAChB,QAAQ,MAAU,KAAS,QAAQ,MAAU,GAAM,CACnD,KAAK,MAAW,OAAO,KAAU,WAAoD,IAAzC,SAAS,eAAe,OAAO,EAAM,CAAC,CAAU;AAE/F,IAAG,gBAAgB,GAAG,EAAW;;AAGnC,QAAO"} |
+1
-1
@@ -24,3 +24,3 @@ { | ||
| }, | ||
| "version": "0.6.7" | ||
| "version": "0.7.0" | ||
| } |
+0
-19
@@ -9,3 +9,2 @@ # @seahax/elemental | ||
| - Direct and safe access to the DOM (no virtual DOM) | ||
| - React-style list rendering using unique keys | ||
| - React-style code reuse using composable hooks | ||
@@ -351,20 +350,2 @@ - Global & local state reactivity | ||
| ## Render Lists With Keys | ||
| ```ts | ||
| // Create a reusable root element. | ||
| const parent = h('div'); | ||
| h(parent, items.map((item) => { | ||
| // No previous children, so all children will be created. | ||
| return h('p', { 'data-key': item.id }, [item.text]); | ||
| })); | ||
| // Update children with matching keys. | ||
| h(parent, items.map((item) => { | ||
| // Second render, so reuse (and update) children with matching keys. | ||
| return h('p', { 'data-key': item.id }, [item.text]); | ||
| })); | ||
| ``` | ||
| ## Combine Conditional Classes | ||
@@ -371,0 +352,0 @@ |
91615
-4.82%813
-3.79%372
-4.86%