diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index 8a3559a6d75b..a57f5adea49d 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -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, @@ -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>(); const uniqueDependenciesCache = new Map>(); type Renderer = (instance: UI5Element, container: HTMLElement | DocumentFragment) => void; @@ -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 @@ -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 @@ -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(); @@ -435,7 +457,7 @@ abstract class UI5Element extends HTMLElement { const autoIncrementMap = new Map(); const slottedChildrenMap = new Map>(); - 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]; @@ -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) @@ -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) => { @@ -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) { @@ -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(); @@ -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) => { diff --git a/packages/base/src/childrenDefinedAndUpgraded.ts b/packages/base/src/childrenDefinedAndUpgraded.ts new file mode 100644 index 000000000000..aacbf8194b4e --- /dev/null +++ b/packages/base/src/childrenDefinedAndUpgraded.ts @@ -0,0 +1,32 @@ +import { shouldIgnoreCustomElement } from "./IgnoreCustomElements.js"; + +const elementTimeouts = new Map>(); + +/** + * 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;