Skip to content

Commit

Permalink
shae-ent: re-request new parent
Browse files Browse the repository at this point in the history
  • Loading branch information
spearwolf committed Jul 5, 2024
1 parent 2d812e5 commit a7120fa
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 12 deletions.
3 changes: 3 additions & 0 deletions packages/shadow-ents-e2e/pages/shae-worker.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<shae-ent id="ent1" ns="local" token="ent1">
<shae-ent id="ent0_2" token="ent0_2"></shae-ent>
<shae-ent id="ent1_1" ns="local" token="ent1_1"></shae-ent>
<element-with-shadow-dom ent-inside="ent-a" ent-slot-container="ant-b" ns="local">
<shae-ent id="ent1_1_1" ns="local" token="ent1_1_1"></shae-ent>
</element-with-shadow-dom>
</shae-ent>
</shae-ent>

Expand Down
7 changes: 7 additions & 0 deletions packages/shadow-ents-e2e/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
Expand Down
36 changes: 36 additions & 0 deletions packages/shadow-ents-e2e/src/shae-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<style>
:host {
display: block;
padding: 1rem;
border: 1px solid #c41;
}
</style>
<shae-ent id="${insideId}" ns="${ns}" token="${insideId}">${insideId}</shae-ent>
<shae-ent id="${slotContainId}" ns="${ns}" token="${slotContainId}">${slotContainId}
<slot></slot>
</shae-ent>
`;

this.addEventListener('slotchange', (event) => {
console.log('<element-with-shadow-dom> slotchange', event);
});

shadowRoot.addEventListener('slotchange', (event) => {
console.log('[ShadowRoot] slotchange', event);
});
}
}

customElements.define('element-with-shadow-dom', ElementWithShadowDom);

main();

async function main() {
Expand Down
108 changes: 97 additions & 11 deletions packages/shadow-ents/src/elements/ShaeEntElement.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand Down Expand Up @@ -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));
Expand All @@ -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}));
}
Expand All @@ -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('<shae-ent> 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());

Expand All @@ -81,7 +119,7 @@ export class ShaeEntElement extends ShaeElement {
}

// --- viewComponent.parent ---
this.#requestEntParent();
this.#dispatchRequestEntParent();
this.#registerEntParentListener();

// --- sync! ---
Expand All @@ -97,6 +135,8 @@ export class ShaeEntElement extends ShaeElement {
}

disconnectedCallback() {
this.#shadowRootHostNeedsUpdate = true;

this.#unregisterEntParentListener();

this.#setEntParent(undefined);
Expand All @@ -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,
Expand All @@ -118,14 +168,25 @@ export class ShaeEntElement extends ShaeElement {
}

#unsubscribeFromEntParent?: () => void;
#nonShaeParents?: WeakSet<HTMLElement>;
#parentsOnTheWayToShae?: WeakSet<HTMLElement>;

#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 <shae-ent> parent so that we can
// request a new parent in case of a custom element upgrade with shae elements in the shadow dom
Expand All @@ -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) {
Expand All @@ -159,13 +226,32 @@ 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;

if (requester === this) return;
if (!requester?.isShaeEntElement) return;
if (requester.ns !== this.ns) return;

event.stopImmediatePropagation();

requester.#setEntParent(this);
};

Expand Down
1 change: 1 addition & 0 deletions packages/shadow-ents/src/elements/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 14 additions & 1 deletion packages/shadow-ents/src/elements/events.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
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: {
requester: ShaeEntElement;
};
}

export interface ReRequestEntParentEvent extends CustomEvent {
detail: {
requester: ShaeEntElement;
shadowRootHost: HTMLElement;
};
}

// TODO remove RequestContextEvent
export interface RequestContextEvent extends CustomEvent {
detail: {
Expand All @@ -18,6 +30,7 @@ export interface RequestContextEvent extends CustomEvent {

export interface ShadowEntsEventMap {
[RequestEntParentEventName]: RequestEntParentEvent;
[ReRequestEntParentEventName]: ReRequestEntParentEvent;
[RequestContextEventName]: RequestContextEvent;
}

Expand Down
21 changes: 21 additions & 0 deletions packages/shadow-ents/src/entities/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ interface EntityEntry {
usedConstructors: Map<ShadowObjectConstructor, Set<ShadowObjectType>>;
}

interface EntityGraphNode {
token: string;
entity: Entity;
children: EntityGraphNode[];
}

enum ShadowObjectAction {
CreateAndDestroy = 0,
JustCreate,
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit a7120fa

Please sign in to comment.