From a7120fa158467b055692877a750629d9e4d68902 Mon Sep 17 00:00:00 2001 From: Wolfger Schramm Date: Fri, 5 Jul 2024 11:29:35 +0200 Subject: [PATCH] shae-ent: re-request new parent --- .../shadow-ents-e2e/pages/shae-worker.html | 3 + packages/shadow-ents-e2e/project.json | 7 ++ packages/shadow-ents-e2e/src/shae-worker.js | 36 ++++++ .../src/elements/ShaeEntElement.ts | 108 ++++++++++++++++-- .../shadow-ents/src/elements/constants.ts | 1 + packages/shadow-ents/src/elements/events.ts | 15 ++- packages/shadow-ents/src/entities/Kernel.ts | 21 ++++ 7 files changed, 179 insertions(+), 12 deletions(-) diff --git a/packages/shadow-ents-e2e/pages/shae-worker.html b/packages/shadow-ents-e2e/pages/shae-worker.html index 62c202a..13930ca 100644 --- a/packages/shadow-ents-e2e/pages/shae-worker.html +++ b/packages/shadow-ents-e2e/pages/shae-worker.html @@ -15,6 +15,9 @@ + + + diff --git a/packages/shadow-ents-e2e/project.json b/packages/shadow-ents-e2e/project.json index e1925cc..afbcfaf 100644 --- a/packages/shadow-ents-e2e/project.json +++ b/packages/shadow-ents-e2e/project.json @@ -24,6 +24,13 @@ }, "outputs": ["{projectRoot}/playwright-report", "{projectRoot}/test-results"], "dependsOn": ["^build", "build"] + }, + "dev": { + "executor": "nx:run-script", + "options": { + "script": "dev" + }, + "dependsOn": ["^build"] } }, "tags": [] diff --git a/packages/shadow-ents-e2e/src/shae-worker.js b/packages/shadow-ents-e2e/src/shae-worker.js index df08189..a74f3c9 100644 --- a/packages/shadow-ents-e2e/src/shae-worker.js +++ b/packages/shadow-ents-e2e/src/shae-worker.js @@ -8,6 +8,42 @@ import {testCustomEvent} from './test-helpers/testCustomEvent.js'; const ContextCreated = ShadowEnv.ContextCreated.toLowerCase(); +class ElementWithShadowDom extends HTMLElement { + constructor() { + super(); + + const shadowRoot = this.attachShadow({mode: this.getAttribute('mode') || 'open'}); + + const insideId = this.getAttribute('ent-inside'); + const slotContainId = this.getAttribute('ent-slot-container'); + const ns = this.getAttribute('ns'); + + shadowRoot.innerHTML = ` + + ${insideId} + ${slotContainId} + + + `; + + this.addEventListener('slotchange', (event) => { + console.log(' slotchange', event); + }); + + shadowRoot.addEventListener('slotchange', (event) => { + console.log('[ShadowRoot] slotchange', event); + }); + } +} + +customElements.define('element-with-shadow-dom', ElementWithShadowDom); + main(); async function main() { diff --git a/packages/shadow-ents/src/elements/ShaeEntElement.ts b/packages/shadow-ents/src/elements/ShaeEntElement.ts index 83bcf62..b599a5f 100644 --- a/packages/shadow-ents/src/elements/ShaeEntElement.ts +++ b/packages/shadow-ents/src/elements/ShaeEntElement.ts @@ -1,7 +1,7 @@ import {beQuiet, createEffect, createSignal} from '@spearwolf/signalize'; import {ComponentContext, ViewComponent} from '../core.js'; import {ShaeElement} from './ShaeElement.js'; -import {ATTR_TOKEN, RequestEntParentEventName} from './constants.js'; +import {ATTR_TOKEN, RequestEntParentEventName, ReRequestEntParentEventName} from './constants.js'; export class ShaeEntElement extends ShaeElement { static override observedAttributes = [...ShaeElement.observedAttributes, ATTR_TOKEN]; @@ -45,6 +45,10 @@ export class ShaeEntElement extends ShaeElement { this.ns$.onChange((ns) => { this.componentContext$.set(ComponentContext.get(ns)); + if (this.isConnected) { + console.log('ns changed', {ns, shaeEnt: this}); + this.#dispatchRequestEntParent(); + } }); this.viewComponent$.onChange((vc) => vc?.destroy.bind(vc)); @@ -56,7 +60,7 @@ export class ShaeEntElement extends ShaeElement { if (vc) { if (context == null) { this.viewComponent$.set(undefined); - } else if (token !== vc.token) { + } else if (token !== vc.token || context !== vc.context) { // TODO make token changeable (ViewComponent) this.viewComponent$.set(new ViewComponent(token, {context})); } @@ -66,9 +70,43 @@ export class ShaeEntElement extends ShaeElement { this.viewComponent$.set(undefined); } }, [this.componentContext$, this.token$]); + + this.addEventListener('slotchange', (event) => { + const shadowRootHost = this.findShadowRootHost(); + + console.debug(' slotchange', {event, shaeEnt: this, shadowRootHost}); + // TODO inform all shae-ents which have the shadowRootHost in their parentsOnTheWayToShae to request a new parent + + this.#dispatchReRequestEntParent(shadowRootHost); + }); + // TODO unsubscribe from slotchange / move to connectedCallback + } + + #shadowRootHost?: HTMLElement; + #shadowRootHostNeedsUpdate = true; + + findShadowRootHost(): HTMLElement | undefined { + if (this.#shadowRootHostNeedsUpdate) { + this.#shadowRootHostNeedsUpdate = false; + + let current: HTMLElement = this; + while (current) { + if (current.parentElement == null) { + const root = current.parentNode as ShadowRoot; + if (root) { + this.#shadowRootHost = root.host as HTMLElement; + } + break; + } + current = current.parentElement; + } + } + return this.#shadowRootHost; } override connectedCallback() { + this.#shadowRootHostNeedsUpdate = true; + // --- token --- beQuiet(() => this.#updateTokenValue()); @@ -81,7 +119,7 @@ export class ShaeEntElement extends ShaeElement { } // --- viewComponent.parent --- - this.#requestEntParent(); + this.#dispatchRequestEntParent(); this.#registerEntParentListener(); // --- sync! --- @@ -97,6 +135,8 @@ export class ShaeEntElement extends ShaeElement { } disconnectedCallback() { + this.#shadowRootHostNeedsUpdate = true; + this.#unregisterEntParentListener(); this.#setEntParent(undefined); @@ -106,8 +146,18 @@ export class ShaeEntElement extends ShaeElement { this.syncShadowObjects(); } - #requestEntParent() { + #dispatchReRequestEntParent(shadowRootHost: HTMLElement) { // https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/ + this.dispatchEvent( + new CustomEvent(ReRequestEntParentEventName, { + bubbles: true, + composed: true, + detail: {requester: this, shadowRootHost}, + }), + ); + } + + #dispatchRequestEntParent() { this.dispatchEvent( new CustomEvent(RequestEntParentEventName, { bubbles: true, @@ -118,14 +168,25 @@ export class ShaeEntElement extends ShaeElement { } #unsubscribeFromEntParent?: () => void; - #nonShaeParents?: WeakSet; + #parentsOnTheWayToShae?: WeakSet; #setEntParent(parent?: ShaeEntElement) { if (this.entParentNode === parent) return; + if (this.entParentNode) { + this.entParentNode.removeEventListener(ReRequestEntParentEventName, this.#onReRequestEntParent, {capture: false}); + } + this.entParentNode = parent; - this.#nonShaeParents = undefined; + if (this.entParentNode) { + this.entParentNode.addEventListener(ReRequestEntParentEventName, this.#onReRequestEntParent, { + capture: false, + passive: false, + }); + } + + this.#parentsOnTheWayToShae = undefined; // we memorize all elements on the way to the parent so that we can // request a new parent in case of a custom element upgrade with shae elements in the shadow dom @@ -134,16 +195,22 @@ export class ShaeEntElement extends ShaeElement { let current = this.parentElement; while (current && current !== parent) { elements.push(current); - current = current.parentElement; + if (current.parentElement == null && current.parentNode) { + current = (current.parentNode as ShadowRoot).host as HTMLElement; + } else { + current = current.parentElement; + } } if (elements.length > 0) { - this.#nonShaeParents = new WeakSet(elements); - console.log('nonShaeParents', {shaeEnt: this, parentsOnTheWayToShae: elements, weakSet: this.#nonShaeParents}); + this.#parentsOnTheWayToShae = new WeakSet(elements); + // console.log('parentsOnTheWayToShae', { + // shaeEnt: this, + // parentsOnTheWayToShae: elements, + // weakSet: this.#parentsOnTheWayToShae, + // }); } } - // TODO dispatch ReRequestEntParent if we are inside a shadowDom! - this.#unsubscribeFromEntParent?.(); if (parent) { @@ -159,6 +226,23 @@ export class ShaeEntElement extends ShaeElement { } } + #onReRequestEntParent = (event: CustomEvent) => { + const requester = event.detail?.requester as ShaeEntElement | undefined; + + if (requester === this) return; + if (!requester?.isShaeEntElement) return; + if (requester.ns !== this.ns) return; + + const shadowRootHost = event.detail?.shadowRootHost as HTMLElement | undefined; + + if (shadowRootHost) { + // console.log('onRequestEntParent', {shaeEnt: this, requester, shadowRootHost}); + if (this.#parentsOnTheWayToShae?.has(shadowRootHost)) { + this.#dispatchRequestEntParent(); + } + } + }; + #onRequestEntParent = (event: CustomEvent) => { const requester = event.detail?.requester as ShaeEntElement | undefined; @@ -166,6 +250,8 @@ export class ShaeEntElement extends ShaeElement { if (!requester?.isShaeEntElement) return; if (requester.ns !== this.ns) return; + event.stopImmediatePropagation(); + requester.#setEntParent(this); }; diff --git a/packages/shadow-ents/src/elements/constants.ts b/packages/shadow-ents/src/elements/constants.ts index c27aaaa..97011fc 100644 --- a/packages/shadow-ents/src/elements/constants.ts +++ b/packages/shadow-ents/src/elements/constants.ts @@ -6,6 +6,7 @@ export enum ShadowElementType { export const RequestContextEventName = 'seRequestContext'; export const RequestEntParentEventName = 'shaeRequestEntParent'; +export const ReRequestEntParentEventName = 'shaeReRequestEntParent'; export const SHADOW_ELEMENT_ENTITY = 'shadow-entity'; export const SHADOW_ELEMENT_ENV = 'shadow-env'; diff --git a/packages/shadow-ents/src/elements/events.ts b/packages/shadow-ents/src/elements/events.ts index eefe59e..0110ceb 100644 --- a/packages/shadow-ents/src/elements/events.ts +++ b/packages/shadow-ents/src/elements/events.ts @@ -1,6 +1,11 @@ import type {ShadowEntityElement} from './ShadowEntityElement.js'; import type {ShaeEntElement} from './ShaeEntElement.js'; -import type {RequestContextEventName, RequestEntParentEventName, ShadowElementType} from './constants.js'; +import type { + RequestContextEventName, + RequestEntParentEventName, + ReRequestEntParentEventName, + ShadowElementType, +} from './constants.js'; export interface RequestEntParentEvent extends CustomEvent { detail: { @@ -8,6 +13,13 @@ export interface RequestEntParentEvent extends CustomEvent { }; } +export interface ReRequestEntParentEvent extends CustomEvent { + detail: { + requester: ShaeEntElement; + shadowRootHost: HTMLElement; + }; +} + // TODO remove RequestContextEvent export interface RequestContextEvent extends CustomEvent { detail: { @@ -18,6 +30,7 @@ export interface RequestContextEvent extends CustomEvent { export interface ShadowEntsEventMap { [RequestEntParentEventName]: RequestEntParentEvent; + [ReRequestEntParentEventName]: ReRequestEntParentEvent; [RequestContextEventName]: RequestContextEvent; } diff --git a/packages/shadow-ents/src/entities/Kernel.ts b/packages/shadow-ents/src/entities/Kernel.ts index f2b3540..d49f05f 100644 --- a/packages/shadow-ents/src/entities/Kernel.ts +++ b/packages/shadow-ents/src/entities/Kernel.ts @@ -27,6 +27,12 @@ interface EntityEntry { usedConstructors: Map>; } +interface EntityGraphNode { + token: string; + entity: Entity; + children: EntityGraphNode[]; +} + enum ShadowObjectAction { CreateAndDestroy = 0, JustCreate, @@ -91,6 +97,21 @@ export class Kernel extends Eventize { .flat(); } + getEntityGraph(): EntityGraphNode[] { + return Array.from(this.#rootEntities).map((uuid) => this.getEntityGraphNode(uuid)!); + } + + private getEntityGraphNode(uuid: string): EntityGraphNode | undefined { + if (!this.#entities.has(uuid)) return undefined; + + const {token, entity} = this.#entities.get(uuid); + return { + token, + entity, + children: entity.children.map((child) => this.getEntityGraphNode(child.uuid)), + }; + } + upgradeEntities(): void { const entities = this.traverseLevelOrderBFS(); const reversedEntities = entities.slice().reverse();