Skip to content

refactor(UI5Element): make _processChildren sync #11471

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 41 additions & 39 deletions packages/base/src/UI5Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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 @@ -43,11 +42,11 @@ import { getI18nBundle } from "./i18nBundle.js";
import type I18nBundle from "./i18nBundle.js";
import { fetchCldr } from "./asset-registries/LocaleData.js";
import getLocale from "./locale/getLocale.js";
import childrenDefinedAndUpgraded from "./childrenDefinedAndUpgraded.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 = (instance: UI5Element, container: HTMLElement | DocumentFragment) => void;
Expand Down Expand Up @@ -278,6 +277,32 @@ abstract class UI5Element extends HTMLElement {
return executeTemplate(template!, this);
}

/**
* @private
* Sets on the host several attributes, unrelated to metadata properties
*/
setNonPropertyAttributes() {
const ctor = this.constructor as typeof UI5Element;

this.setAttribute(ctor.getMetadata().getPureTag(), "");
if (ctor.getMetadata().supportsF6FastNavigation()) {
this.setAttribute("data-sap-ui-fastnavgroup", "true");
}
}

/**
* @private
* Stripped down version of connectedCallback, usable in SSR scenarios
*/
serverRender() {
this.setNonPropertyAttributes(); // set pure tag attribute
this.onBeforeRendering(); // onBeforeRendering hook must run before updateAttributes (many private properties are set there)
this.updateAttributes(); // set property-related attributes
this._processChildren(); // set slots accessors
this._assignIndividualSlotsToChildren();
return updateShadowRoot(this); // render as string
}

/**
* Do not call this method from derivatives of UI5Element, use "onEnterDOM" only
* @private
Expand All @@ -294,21 +319,18 @@ abstract class UI5Element extends HTMLElement {
}
}

const ctor = this.constructor as typeof UI5Element;

this.setAttribute(ctor.getMetadata().getPureTag(), "");
if (ctor.getMetadata().supportsF6FastNavigation()) {
this.setAttribute("data-sap-ui-fastnavgroup", "true");
}
this.setNonPropertyAttributes();

const ctor = this.constructor as typeof UI5Element;
const slotsAreManaged = ctor.getMetadata().slotsAreManaged();

this._inDOM = true;

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

if (!this._inDOM) { // Component removed from DOM while _processChildren was running
Expand Down Expand Up @@ -405,17 +427,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 @@ -435,7 +457,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 @@ -457,26 +479,6 @@ abstract class UI5Element extends HTMLElement {
(child as SlottedChild)._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);
}
}

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)
Expand All @@ -499,8 +501,6 @@ abstract class UI5Element extends HTMLElement {
}
});

await Promise.all(allChildrenUpgraded);

// Distribute the child in the _state object, keeping the Light DOM order,
// not the order elements are defined.
slottedChildrenMap.forEach((children, propertyName) => {
Expand Down Expand Up @@ -805,7 +805,6 @@ abstract class UI5Element extends HTMLElement {
*/
_render() {
const ctor = this.constructor as typeof UI5Element;
const hasIndividualSlots = ctor.getMetadata().hasIndividualSlots();

// restore properties that were initialized before `define` by calling the setter
if (this.initializedProperties.size > 0) {
Expand Down Expand Up @@ -863,9 +862,7 @@ abstract class UI5Element extends HTMLElement {
this._rendered = true;

// Safari requires that children get the slot attribute only after the slot tags have been rendered in the shadow DOM
if (hasIndividualSlots) {
this._assignIndividualSlotsToChildren();
}
this._assignIndividualSlotsToChildren();

// Call the onAfterRendering hook
this.onAfterRendering();
Expand All @@ -875,6 +872,11 @@ abstract class UI5Element extends HTMLElement {
* @private
*/
_assignIndividualSlotsToChildren() {
const ctor = this.constructor as typeof UI5Element;
const hasIndividualSlots = ctor.getMetadata().hasIndividualSlots();
if (!hasIndividualSlots) {
return;
}
const domChildren = Array.from(this.children);

domChildren.forEach((child: Record<string, any>) => {
Expand Down
32 changes: 32 additions & 0 deletions packages/base/src/childrenDefinedAndUpgraded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { shouldIgnoreCustomElement } from "./IgnoreCustomElements.js";

const elementTimeouts = new Map<string, Promise<void>>();

/**
* Awaits for all direct children that are custom elements to be defined and force upgrades them
*/
const childrenDefinedAndUpgraded = (el: HTMLElement) => {
return Promise.all([...el.children].map(async child => {
if (child instanceof HTMLSlotElement) {
await childrenDefinedAndUpgraded(child);
} else {
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);
}
}
}));
};

export default childrenDefinedAndUpgraded;
Loading