Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: make processChildren sync #9890

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
135 changes: 71 additions & 64 deletions packages/base/src/UI5Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
} from "./UI5ElementMetadata.js";
import EventProvider from "./EventProvider.js";
import updateShadowRoot from "./updateShadowRoot.js";
import { shouldIgnoreCustomElement } from "./IgnoreCustomElements.js";
import {
renderDeferred,
renderImmediately,
Expand Down Expand Up @@ -45,7 +44,6 @@ import getLocale from "./locale/getLocale.js";
const DEV_MODE = true;
let autoId = 0;

const elementTimeouts = new Map<string, Promise<void>>();
const uniqueDependenciesCache = new Map<typeof UI5Element, Array<typeof UI5Element>>();

type Renderer = (templateResult: TemplateFunctionResult, container: HTMLElement | DocumentFragment, options: RendererOptions) => void;
Expand All @@ -70,7 +68,12 @@ type ChangeInfo = {

type InvalidationInfo = ChangeInfo & { target: UI5Element };

type ChildChangeListener = (param: InvalidationInfo) => void;
type ChildChangeListener = (e: CustomEvent<ChangeInfo>) => void;
declare global {
interface HTMLElementEventMap {
"ui5-child-invalidate": CustomEvent<ChangeInfo>;
}
}

type SlotChangeListener = (this: HTMLSlotElement, ev: Event) => void;

Expand Down Expand Up @@ -121,6 +124,10 @@ function _invalidate(this: UI5Element, changeInfo: ChangeInfo) {
this._changedState.push(changeInfo);
renderDeferred(this);
this._invalidationEventProvider.fireEvent("invalidate", { ...changeInfo, target: this });
if (this._fireInvalidations) {
this.parentElement?.dispatchEvent(new CustomEvent("ui5-child-invalidate", { detail: { ...changeInfo, target: this } }));
// this.assignedSlot?.parentElement?.dispatchEvent(new CustomEvent("ui5-child-invalidate", { detail: { ...changeInfo, target: this } }));
}
}

/**
Expand Down Expand Up @@ -153,7 +160,6 @@ abstract class UI5Element extends HTMLElement {
_changedState: Array<ChangeInfo>;
_invalidationEventProvider: EventProvider<InvalidationInfo, void>;
_componentStateFinalizedEventProvider: EventProvider<void, void>;
_inDOM: boolean;
_fullyConnected: boolean;
_childChangeListeners: Map<string, ChildChangeListener>;
_slotsAssignedNodes: WeakMap<HTMLSlotElement, Array<SlotValue>>;
Expand All @@ -163,6 +169,7 @@ abstract class UI5Element extends HTMLElement {
_state: State;
_internals: ElementInternals;
_getRealDomRef?: () => HTMLElement;
_fireInvalidations?: boolean;

static template?: TemplateFunction;
static _metadata: UI5ElementMetadata;
Expand All @@ -181,7 +188,6 @@ abstract class UI5Element extends HTMLElement {
const ctor = this.constructor as typeof UI5Element;
this._changedState = []; // Filled on each invalidation, cleared on re-render (used for debugging)
this._suppressInvalidation = true; // A flag telling whether all invalidations should be ignored. Initialized with "true" because a UI5Element can not be invalidated until it is rendered for the first time
this._inDOM = false; // A flag telling whether the UI5Element is currently in the DOM tree of the document or not
this._fullyConnected = false; // A flag telling whether the UI5Element's onEnterDOM hook was called (since it's possible to have the element removed from DOM before that)
this._childChangeListeners = new Map(); // used to store lazy listeners per slot for the child change event of every child inside that slot
this._slotChangeListeners = new Map(); // used to store lazy listeners per slot for the slotchange event of all slot children inside that slot
Expand Down Expand Up @@ -291,16 +297,10 @@ abstract class UI5Element extends HTMLElement {

const slotsAreManaged = ctor.getMetadata().slotsAreManaged();

this._inDOM = true;

if (slotsAreManaged) {
// always register the observer before yielding control to the main thread (await)
this._startObservingDOMChildren();
await this._processChildren();
}

if (!this._inDOM) { // Component removed from DOM while _processChildren was running
return;
this._processChildren();
}

if (!ctor.asyncFinished) {
Expand All @@ -321,8 +321,6 @@ abstract class UI5Element extends HTMLElement {
const ctor = this.constructor as typeof UI5Element;
const slotsAreManaged = ctor.getMetadata().slotsAreManaged();

this._inDOM = false;

if (slotsAreManaged) {
this._stopObservingDOMChildren();
}
Expand Down Expand Up @@ -393,17 +391,17 @@ abstract class UI5Element extends HTMLElement {
* Note: this method is also manually called by "compatibility/patchNodeValue.js"
* @private
*/
async _processChildren() {
_processChildren() {
const hasSlots = (this.constructor as typeof UI5Element).getMetadata().hasSlots();
if (hasSlots) {
await this._updateSlots();
this._updateSlots();
}
}

/**
* @private
*/
async _updateSlots() {
_updateSlots() {
const ctor = this.constructor as typeof UI5Element;
const slotsMap = ctor.getMetadata().getSlots();
const canSlotText = ctor.getMetadata().canSlotText();
Expand All @@ -423,7 +421,7 @@ abstract class UI5Element extends HTMLElement {
const autoIncrementMap = new Map<string, number>();
const slottedChildrenMap = new Map<string, Array<{child: Node, idx: number }>>();

const allChildrenUpgraded = domChildren.map(async (child, idx) => {
domChildren.forEach((child, idx) => {
// Determine the type of the child (mainly by the slot attribute)
const slotName = getSlotName(child);
const slotData = slotsMap[slotName];
Expand All @@ -445,32 +443,34 @@ abstract class UI5Element extends HTMLElement {
(child as Record<string, any>)._individualSlot = `${slotName}-${nextIndex}`;
}

// Await for not-yet-defined custom elements
if (child instanceof HTMLElement) {
const localName = child.localName;
const shouldWaitForCustomElement = localName.includes("-") && !shouldIgnoreCustomElement(localName);

if (shouldWaitForCustomElement) {
const isDefined = customElements.get(localName);
if (!isDefined) {
const whenDefinedPromise = customElements.whenDefined(localName); // Class registered, but instances not upgraded yet
let timeoutPromise = elementTimeouts.get(localName);
if (!timeoutPromise) {
timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
elementTimeouts.set(localName, timeoutPromise);
}
await Promise.race([whenDefinedPromise, timeoutPromise]);
}
customElements.upgrade(child);
}
}
// // Await for not-yet-defined custom elements
// if (child instanceof HTMLElement) {
// const localName = child.localName;
// const shouldWaitForCustomElement = localName.includes("-") && !shouldIgnoreCustomElement(localName);

// if (shouldWaitForCustomElement) {
// const isDefined = customElements.get(localName);
// if (!isDefined) {
// const whenDefinedPromise = customElements.whenDefined(localName); // Class registered, but instances not upgraded yet
// let timeoutPromise = elementTimeouts.get(localName);
// if (!timeoutPromise) {
// timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
// elementTimeouts.set(localName, timeoutPromise);
// }
// await Promise.race([whenDefinedPromise, timeoutPromise]);
// }
// customElements.upgrade(child);
// }
// }

child = (ctor.getMetadata().constructor as typeof UI5ElementMetadata).validateSlotValue(child, slotData);

// Listen for any invalidation on the child if invalidateOnChildChange is true or an object (ignore when false or not set)
if (instanceOfUI5Element(child) && slotData.invalidateOnChildChange) {
if (slotData.invalidateOnChildChange) {
const childChangeListener = this._getChildChangeListener(slotName);
child.attachInvalidate.call(child, childChangeListener);
// child.attachInvalidate.call(child, childChangeListener);
(child as UI5Element)._fireInvalidations = true;
this.addEventListener("ui5-child-invalidate", childChangeListener);
}

// Listen for the slotchange event if the child is a slot itself
Expand All @@ -487,7 +487,7 @@ abstract class UI5Element extends HTMLElement {
}
});

await Promise.all(allChildrenUpgraded);
// await Promise.all(allChildrenUpgraded);

// Distribute the child in the _state object, keeping the Light DOM order,
// not the order elements are defined.
Expand Down Expand Up @@ -536,8 +536,9 @@ abstract class UI5Element extends HTMLElement {

children.forEach(child => {
if (instanceOfUI5Element(child)) {
const childChangeListener = this._getChildChangeListener(slotName);
child.detachInvalidate.call(child, childChangeListener);
// const childChangeListener = this._getChildChangeListener(slotName);
// child.detachInvalidate.call(child, childChangeListener);
child._fireInvalidations = false;
}

if (child instanceof HTMLSlotElement) {
Expand Down Expand Up @@ -573,10 +574,16 @@ abstract class UI5Element extends HTMLElement {
* Callback that is executed whenever a monitored child changes its state
*
* @param slotName the slot in which a child was invalidated
* @param childChangeInfo the changeInfo object for the child in the given slot
* @param e the changeInfo object for the child in the given slot
* @private
*/
_onChildChange(slotName: string, childChangeInfo: ChangeInfo) {
_onChildChange(slotName: string, e: CustomEvent<ChangeInfo>) {
// const slotName = (e.target as HTMLElement).slot;
const childChangeInfo = e.detail;
// startsWith for individual slots
if (!(childChangeInfo.target?.slot || "default").startsWith(slotName)) {
return;
}
if (!(this.constructor as typeof UI5Element).getMetadata().shouldInvalidateOnChildChange(slotName, childChangeInfo.type, childChangeInfo.name)) {
return;
}
Expand Down Expand Up @@ -704,26 +711,26 @@ abstract class UI5Element extends HTMLElement {
slotChangeListener.call(slot, e);

if (invalidateOnChildChange) {
// Detach listeners for UI5 Elements that used to be in this slot
const previousChildren = this._slotsAssignedNodes.get(slot);
if (previousChildren) {
previousChildren.forEach(child => {
if (instanceOfUI5Element(child)) {
const childChangeListener = this._getChildChangeListener(slotName);
child.detachInvalidate.call(child, childChangeListener);
}
});
}

// Attach listeners for UI5 Elements that are now in this slot
const newChildren = getSlottedNodesList([slot]);
this._slotsAssignedNodes.set(slot, newChildren);
newChildren.forEach(child => {
if (instanceOfUI5Element(child)) {
const childChangeListener = this._getChildChangeListener(slotName);
child.attachInvalidate.call(child, childChangeListener);
}
});
// // Detach listeners for UI5 Elements that used to be in this slot
// const previousChildren = this._slotsAssignedNodes.get(slot);
// if (previousChildren) {
// previousChildren.forEach(child => {
// if (instanceOfUI5Element(child)) {
// const childChangeListener = this._getChildChangeListener(slotName);
// child.detachInvalidate.call(child, childChangeListener);
// }
// });
// }

// // Attach listeners for UI5 Elements that are now in this slot
// const newChildren = getSlottedNodesList([slot]);
// this._slotsAssignedNodes.set(slot, newChildren);
// newChildren.forEach(child => {
// if (instanceOfUI5Element(child)) {
// const childChangeListener = this._getChildChangeListener(slotName);
// child.attachInvalidate.call(child, childChangeListener);
// }
// });
}
});
}
Expand Down