From efa3375f3113ea5a9ffcce0aca87982acc484e6b Mon Sep 17 00:00:00 2001 From: Kevin Bulteel Date: Mon, 8 Jan 2024 23:37:06 +0100 Subject: [PATCH] feature: nested component --- .../ui/example/nested-component/a.html | 2 + .../@hec.js/ui/example/nested-component/a.js | 6 ++ .../ui/example/nested-component/b.html | 1 + .../@hec.js/ui/example/nested-component/b.js | 6 ++ .../ui/example/nested-component/index.html | 15 +++++ packages/@hec.js/ui/lib/src/component.js | 64 ++++++++++++++++--- .../@hec.js/ui/lib/src/plugins/data-bind.js | 2 +- .../@hec.js/ui/lib/src/plugins/data-for.js | 2 +- .../@hec.js/ui/lib/src/plugins/data-if.js | 2 +- .../@hec.js/ui/lib/src/plugins/data-on.js | 2 +- .../@hec.js/ui/lib/src/{value.js => props.js} | 9 +++ packages/@hec.js/ui/lib/src/signal.js | 2 +- packages/@hec.js/ui/lib/src/template.js | 13 ++-- 13 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 packages/@hec.js/ui/example/nested-component/a.html create mode 100644 packages/@hec.js/ui/example/nested-component/a.js create mode 100644 packages/@hec.js/ui/example/nested-component/b.html create mode 100644 packages/@hec.js/ui/example/nested-component/b.js create mode 100644 packages/@hec.js/ui/example/nested-component/index.html rename packages/@hec.js/ui/lib/src/{value.js => props.js} (87%) diff --git a/packages/@hec.js/ui/example/nested-component/a.html b/packages/@hec.js/ui/example/nested-component/a.html new file mode 100644 index 0000000..4324b10 --- /dev/null +++ b/packages/@hec.js/ui/example/nested-component/a.html @@ -0,0 +1,2 @@ +
{{ text }}
+ \ No newline at end of file diff --git a/packages/@hec.js/ui/example/nested-component/a.js b/packages/@hec.js/ui/example/nested-component/a.js new file mode 100644 index 0000000..0ffa727 --- /dev/null +++ b/packages/@hec.js/ui/example/nested-component/a.js @@ -0,0 +1,6 @@ +import { component, templateByName } from '../../lib/index.js'; + +component('c-a', { text: 'Hi' }, (props) => { + + return templateByName('./a.html', props); +}); \ No newline at end of file diff --git a/packages/@hec.js/ui/example/nested-component/b.html b/packages/@hec.js/ui/example/nested-component/b.html new file mode 100644 index 0000000..f4d2fb4 --- /dev/null +++ b/packages/@hec.js/ui/example/nested-component/b.html @@ -0,0 +1 @@ +
{{ text }}
\ No newline at end of file diff --git a/packages/@hec.js/ui/example/nested-component/b.js b/packages/@hec.js/ui/example/nested-component/b.js new file mode 100644 index 0000000..fbb7101 --- /dev/null +++ b/packages/@hec.js/ui/example/nested-component/b.js @@ -0,0 +1,6 @@ +import { component, templateByName } from '../../lib/index.js'; + +component('c-b', { text: '' }, (props) => { + + return templateByName('./b.html', props); +}); \ No newline at end of file diff --git a/packages/@hec.js/ui/example/nested-component/index.html b/packages/@hec.js/ui/example/nested-component/index.html new file mode 100644 index 0000000..84af1a4 --- /dev/null +++ b/packages/@hec.js/ui/example/nested-component/index.html @@ -0,0 +1,15 @@ + + + + + + hec.js :: Component + + + + + + + + + diff --git a/packages/@hec.js/ui/lib/src/component.js b/packages/@hec.js/ui/lib/src/component.js index b109bda..69bcb6f 100644 --- a/packages/@hec.js/ui/lib/src/component.js +++ b/packages/@hec.js/ui/lib/src/component.js @@ -1,4 +1,5 @@ -import { signal } from "./signal.js"; +import { f, nodeProps, prop, propsOf } from "./props.js"; +import { isSignal, signal } from "./signal.js"; /** * @template T @@ -22,15 +23,23 @@ export function component(name, props, fn) { /** @type { {[R in keyof T]: import("./signal.js").Signal} } */// @ts-ignore #signals = Object.fromEntries(Object.entries(props).map(e => [e[0], signal(e[1])])); + /** @type { AbortController[] } */ + #aborts = []; + + /** @type {{ [key: string]: AbortController }} */ + #parentSignalAborts = {}; + connectedCallback() { const shadow = this.attachShadow({ mode: 'open' }), - node = fn(this.#signals); + node = fn(this.#signals), + abort = new AbortController(); /** @param { Node } node */ const append = (node) => { - shadow.append(node); - this.dispatchEvent(new CustomEvent('::mount')); + nodeProps.set(this, propsOf(node)); + shadow.append(node); + for (const k in this.#signals) { if (!this.hasAttribute(k)) { @@ -39,18 +48,36 @@ export function component(name, props, fn) { this.#signals[k].subscribe({ next: v => this.setAttribute(k, v.toString()) - }); + }, { signal: abort.signal }); } + + this.#aborts.push(abort); + + this.dispatchEvent(new CustomEvent('::mount')); } node instanceof Promise ? node.then(append) : append(node); } disconnectedCallback() { + nodeProps.delete(this); + + while (this.#aborts.length) { + this.#aborts.pop().abort(); + } + + for (const k in this.#parentSignalAborts) { + this.#parentSignalAborts[k].abort(); + delete this.#parentSignalAborts[k]; + } + this.dispatchEvent(new CustomEvent('::unmount')); } - /** @param { string} key */ + /** + * @param { string } key + * @returns { [ any, import("./signal.js").Signal ] } + */ #propSignalByLowerKey(key = '') { key = key.toLowerCase(); @@ -60,7 +87,7 @@ export function component(name, props, fn) { } } - return []; + return [ null, null]; } /** @@ -69,9 +96,28 @@ export function component(name, props, fn) { * @param { string } value */ attributeChangedCallback(name, _, value) { - const [ prop, signal ] = this.#propSignalByLowerKey(name); + const [ p, signal ] = this.#propSignalByLowerKey(name); + + if (value.startsWith('@parent.')) { // @ts-ignore + const parent = this.parentNode.host || this.parentNode, + parentProps = propsOf(parent), + parentProp = prop(parentProps, value.slice(8)); - if (typeof prop == 'number') { + if (isSignal(parentProp)) { + this.#parentSignalAborts[p]?.abort(); + this.#parentSignalAborts[p] = new AbortController(); + + signal(f(parentProp)); + + parentProp.subscribe({ next: signal }, { + signal: this.#parentSignalAborts[p].signal + }); + + } else { + signal(parentProp); + } + + } else if (typeof p == 'number') { // @ts-ignore signal(parseFloat(value)); } else { diff --git a/packages/@hec.js/ui/lib/src/plugins/data-bind.js b/packages/@hec.js/ui/lib/src/plugins/data-bind.js index f9770a6..dbb7083 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-bind.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-bind.js @@ -1,5 +1,5 @@ import { isSignal } from "../signal.js"; -import { f, prop } from "../value.js"; +import { f, prop } from "../props.js"; /** @type {{ [key: string]: (node: HTMLInputElement) => any }} */ const valueByType = { diff --git a/packages/@hec.js/ui/lib/src/plugins/data-for.js b/packages/@hec.js/ui/lib/src/plugins/data-for.js index 694925c..df6424b 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-for.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-for.js @@ -1,6 +1,6 @@ import { isSignal } from "../signal.js"; import { templateByNode } from "../template.js"; -import { f, prop } from "../value.js"; +import { f, prop } from "../props.js"; const done = new WeakSet(); diff --git a/packages/@hec.js/ui/lib/src/plugins/data-if.js b/packages/@hec.js/ui/lib/src/plugins/data-if.js index 43b5e5a..f7e673c 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-if.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-if.js @@ -1,5 +1,5 @@ import { isSignal } from "../signal.js"; -import { f, prop } from "../value.js"; +import { f, prop } from "../props.js"; /** * @type { import("../plugins.js").Plugin } diff --git a/packages/@hec.js/ui/lib/src/plugins/data-on.js b/packages/@hec.js/ui/lib/src/plugins/data-on.js index 0c6af6a..18df717 100644 --- a/packages/@hec.js/ui/lib/src/plugins/data-on.js +++ b/packages/@hec.js/ui/lib/src/plugins/data-on.js @@ -1,4 +1,4 @@ -import { prop } from "../value.js"; +import { prop } from "../props.js"; /** @type { import("../plugins.js").Plugin } */ export const dataOnPlugin = { diff --git a/packages/@hec.js/ui/lib/src/value.js b/packages/@hec.js/ui/lib/src/props.js similarity index 87% rename from packages/@hec.js/ui/lib/src/value.js rename to packages/@hec.js/ui/lib/src/props.js index b8d096b..80887fe 100644 --- a/packages/@hec.js/ui/lib/src/value.js +++ b/packages/@hec.js/ui/lib/src/props.js @@ -1,5 +1,14 @@ import { isSignal, signal } from "./signal.js"; +/** + * @template T + * @type { WeakMap } + */ +export const nodeProps = new WeakMap(); + +/** @param { Node } node */ +export const propsOf = (node) => nodeProps.get(node); + /** * @param { any | function(): any} v * @returns { any } diff --git a/packages/@hec.js/ui/lib/src/signal.js b/packages/@hec.js/ui/lib/src/signal.js index 287043f..d5491fd 100644 --- a/packages/@hec.js/ui/lib/src/signal.js +++ b/packages/@hec.js/ui/lib/src/signal.js @@ -1,4 +1,4 @@ -import { f } from "./value.js"; +import { f } from "./props.js"; /** * @template T diff --git a/packages/@hec.js/ui/lib/src/template.js b/packages/@hec.js/ui/lib/src/template.js index e853d6a..3f84d93 100644 --- a/packages/@hec.js/ui/lib/src/template.js +++ b/packages/@hec.js/ui/lib/src/template.js @@ -2,10 +2,7 @@ import { expression } from './expression.js'; import { pipes } from './pipes.js'; import { plugins } from './plugins.js'; import { isSignal } from './signal.js'; -import { f, prop } from './value.js'; - -/** @type { WeakSet } */ -const done = new WeakSet(); +import { f, nodeProps, prop } from './props.js'; /** @type {{ [key: string]: Promise }} */ const templatesLoading = {} @@ -66,7 +63,7 @@ export function templateByString(template, props = {}) { */ export function templateByNode(template, props = {}) { - if (done.has(template)) { + if (nodeProps.has(template)) { return; } @@ -144,7 +141,7 @@ export function templateByNode(template, props = {}) { const findExpression = (node) => { const parentNode = node.parentNode; - done.add(node); + nodeProps.set(node, props); if (node instanceof HTMLElement) { @@ -161,9 +158,13 @@ export function templateByNode(template, props = {}) { const attribute = node.getAttribute(attributeName); if (attribute.includes('{{')) { + bindExpressions(attribute, (text) => { node.setAttribute(attributeName, text.trim().replace(/ +/, ' ')) }); + + } else if (node.localName.includes('-') && props[attribute]) { + node.setAttribute(attributeName, `@parent.${attribute}`); } } }