Skip to content

Commit d55eba8

Browse files
authored
feat: enhance feature initialization (#9479)
Previously, feature loading depended on the import order, requiring features to be imported before the component definition. This caused issues in some situations, especially when creating chunks because the imports can be reordered by the build tools. With this change, we have split the features into two types: library-specific and component-specific. Library-specific features need to be imported before the boot process, otherwise, it can cause serious issues, because the need to re-render all components and manipulate the DOM (including scripts, styles, and meta tags). Component-specific features can now be imported without a specific order, and components that depend on these features will automatically update, enabling the feature on the next rendering of the component. Fixes: #8175
1 parent 079ee04 commit d55eba8

File tree

11 files changed

+157
-68
lines changed

11 files changed

+157
-68
lines changed

packages/base/src/FeaturesRegistry.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1+
import EventProvider from "./EventProvider.js";
2+
import type UI5Element from "./UI5Element.js";
3+
4+
abstract class ComponentFeature {
5+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-function
6+
constructor(...args: any[]) {}
7+
static define?: () => Promise<void>;
8+
static dependencies?: Array<typeof UI5Element>
9+
}
10+
111
const features = new Map<string, any>();
12+
const componentFeatures = new Map<string, ComponentFeature>();
13+
const subscribers = new Map<typeof UI5Element, Array<string>>();
14+
15+
const EVENT_NAME = "componentFeatureLoad";
16+
const eventProvider = new EventProvider<undefined, void>();
17+
18+
const featureLoadEventName = (name: string) => `${EVENT_NAME}_${name}`;
219

320
const registerFeature = (name: string, feature: object) => {
421
features.set(name, feature);
@@ -8,7 +25,44 @@ const getFeature = <T>(name: string): T => {
825
return features.get(name) as T;
926
};
1027

28+
const registerComponentFeature = async (name: string, feature: typeof ComponentFeature) => {
29+
await Promise.all(feature.dependencies?.map(dep => dep.define()) || []);
30+
await feature.define?.();
31+
32+
componentFeatures.set(name, feature);
33+
notifyForFeatureLoad(name);
34+
};
35+
36+
const getComponentFeature = <T>(name: string): T => {
37+
return componentFeatures.get(name) as T;
38+
};
39+
40+
const subscribeForFeatureLoad = (name: string, klass: typeof UI5Element, callback: () => void) => {
41+
const subscriber = subscribers.get(klass);
42+
const isSubscribed = subscriber?.includes(name);
43+
44+
if (isSubscribed) {
45+
return;
46+
}
47+
48+
if (!subscriber) {
49+
subscribers.set(klass, [name]);
50+
} else {
51+
subscriber.push(name);
52+
}
53+
54+
eventProvider.attachEvent(featureLoadEventName(name), callback);
55+
};
56+
57+
const notifyForFeatureLoad = (name: string) => {
58+
eventProvider.fireEvent(featureLoadEventName(name), undefined);
59+
};
60+
1161
export {
1262
registerFeature,
1363
getFeature,
64+
registerComponentFeature,
65+
getComponentFeature,
66+
subscribeForFeatureLoad,
67+
ComponentFeature,
1468
};

packages/base/src/UI5Element.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type {
3737
} from "./types.js";
3838
import { attachFormElementInternals, setFormValue } from "./features/InputElementsFormSupport.js";
3939
import type { IFormInputElement } from "./features/InputElementsFormSupport.js";
40+
import { subscribeForFeatureLoad } from "./FeaturesRegistry.js";
4041

4142
const DEV_MODE = true;
4243
let autoId = 0;
@@ -1168,15 +1169,19 @@ abstract class UI5Element extends HTMLElement {
11681169
return [];
11691170
}
11701171

1172+
static cacheUniqueDependencies(this: typeof UI5Element): void {
1173+
const filtered = this.dependencies.filter((dep, index, deps) => deps.indexOf(dep) === index);
1174+
uniqueDependenciesCache.set(this, filtered);
1175+
}
1176+
11711177
/**
11721178
* Returns a list of the unique dependencies for this UI5 Web Component
11731179
*
11741180
* @public
11751181
*/
11761182
static getUniqueDependencies(this: typeof UI5Element): Array<typeof UI5Element> {
11771183
if (!uniqueDependenciesCache.has(this)) {
1178-
const filtered = this.dependencies.filter((dep, index, deps) => deps.indexOf(dep) === index);
1179-
uniqueDependenciesCache.set(this, filtered);
1184+
this.cacheUniqueDependencies();
11801185
}
11811186

11821187
return uniqueDependenciesCache.get(this) || [];
@@ -1212,6 +1217,12 @@ abstract class UI5Element extends HTMLElement {
12121217

12131218
const tag = this.getMetadata().getTag();
12141219

1220+
const features = this.getMetadata().getFeatures();
1221+
1222+
features.forEach(feature => {
1223+
subscribeForFeatureLoad(feature, this, this.cacheUniqueDependencies.bind(this));
1224+
});
1225+
12151226
const definedLocally = isTagRegistered(tag);
12161227
const definedGlobally = customElements.get(tag);
12171228

packages/base/src/UI5ElementMetadata.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Metadata = {
4141
languageAware?: boolean,
4242
formAssociated?: boolean,
4343
shadowRootOptions?: Partial<ShadowRootInit>
44+
features?: Array<string>
4445
};
4546

4647
type State = Record<string, PropertyValue | Array<SlotValue>>;
@@ -96,6 +97,14 @@ class UI5ElementMetadata {
9697
return this.metadata.tag || "";
9798
}
9899

100+
/**
101+
* Returns the tag of the UI5 Element without the scope
102+
* @private
103+
*/
104+
getFeatures(): Array<string> {
105+
return this.metadata.features || [];
106+
}
107+
99108
/**
100109
* Returns the tag of the UI5 Element
101110
* @public

packages/base/src/decorators/customElement.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const customElement = (tagNameOrComponentSettings: string | {
2020
fastNavigation?: boolean,
2121
formAssociated?: boolean,
2222
shadowRootOptions?: Partial<ShadowRootInit>,
23+
features?: Array<string>,
2324
} = {}): ClassDecorator => {
2425
return (target: any) => {
2526
if (!Object.prototype.hasOwnProperty.call(target, "metadata")) {
@@ -38,12 +39,18 @@ const customElement = (tagNameOrComponentSettings: string | {
3839
fastNavigation,
3940
formAssociated,
4041
shadowRootOptions,
42+
features,
4143
} = tagNameOrComponentSettings;
4244

4345
target.metadata.tag = tag;
4446
if (languageAware) {
4547
target.metadata.languageAware = languageAware;
4648
}
49+
50+
if (features) {
51+
target.metadata.features = features;
52+
}
53+
4754
if (themeAware) {
4855
target.metadata.themeAware = themeAware;
4956
}

packages/main/src/ColorPalette.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
isUp,
1818
isTabNext,
1919
} from "@ui5/webcomponents-base/dist/Keys.js";
20-
import { getFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js";
20+
import { getComponentFeature } from "@ui5/webcomponents-base/dist/FeaturesRegistry.js";
2121
import ColorPaletteTemplate from "./generated/templates/ColorPaletteTemplate.lit.js";
2222
import ColorPaletteItem from "./ColorPaletteItem.js";
2323
import Button from "./Button.js";
@@ -73,10 +73,11 @@ type ColorPaletteItemClickEventDetail = {
7373
@customElement({
7474
tag: "ui5-color-palette",
7575
renderer: litRender,
76+
features: ["ColorPaletteMoreColors"],
7677
template: ColorPaletteTemplate,
7778
styles: [ColorPaletteCss, ColorPaletteDialogCss],
7879
get dependencies() {
79-
const colorPaletteMoreColors = getFeature<typeof ColorPaletteMoreColors>("ColorPaletteMoreColors");
80+
const colorPaletteMoreColors = getComponentFeature<typeof ColorPaletteMoreColors>("ColorPaletteMoreColors");
8081
return ([ColorPaletteItem, Button] as Array<typeof UI5Element>).concat(colorPaletteMoreColors ? colorPaletteMoreColors.dependencies : []);
8182
},
8283
})
@@ -172,19 +173,14 @@ class ColorPalette extends UI5Element {
172173
_itemNavigation: ItemNavigation;
173174
_itemNavigationRecentColors: ItemNavigation;
174175
_recentColors: Array<string>;
175-
moreColorsFeature: ColorPaletteMoreColors | Record<string, any> = {};
176+
moreColorsFeature?: ColorPaletteMoreColors;
176177
_currentlySelected?: ColorPaletteItem;
177178
_shouldFocusRecentColors = false;
178179

179180
static i18nBundle: I18nBundle;
180181

181182
static async onDefine() {
182-
const colorPaletteMoreColors = getFeature<typeof ColorPaletteMoreColors>("ColorPaletteMoreColors");
183-
184-
[ColorPalette.i18nBundle] = await Promise.all([
185-
getI18nBundle("@ui5/webcomponents"),
186-
colorPaletteMoreColors ? colorPaletteMoreColors.init() : Promise.resolve(),
187-
]);
183+
ColorPalette.i18nBundle = await getI18nBundle("@ui5/webcomponents");
188184
}
189185

190186
constructor() {
@@ -218,17 +214,19 @@ class ColorPalette extends UI5Element {
218214
});
219215

220216
if (this.showMoreColors) {
221-
const ColorPaletteMoreColorsClass = getFeature<typeof ColorPaletteMoreColors>("ColorPaletteMoreColors");
217+
const ColorPaletteMoreColorsClass = getComponentFeature<typeof ColorPaletteMoreColors>("ColorPaletteMoreColors");
222218
if (ColorPaletteMoreColorsClass) {
223219
this.moreColorsFeature = new ColorPaletteMoreColorsClass();
224-
} else {
225-
throw new Error(`You have to import "@ui5/webcomponents/dist/features/ColorPaletteMoreColors.js" module to use the more-colors functionality.`);
226220
}
227221
}
228222

229223
this.onPhone = isPhone();
230224
}
231225

226+
get _effectiveShowMoreColors() {
227+
return !!(this.showMoreColors && this.moreColorsFeature);
228+
}
229+
232230
onAfterRendering() {
233231
if (this._shouldFocusRecentColors && this.hasRecentColors) {
234232
this.recentColorsElements[0].selected = true;
@@ -528,6 +526,18 @@ class ColorPalette extends UI5Element {
528526
return [...this.effectiveColorItems, ...this.recentColorsElements];
529527
}
530528

529+
get colorPaletteDialogTitle() {
530+
return this.moreColorsFeature?.colorPaletteDialogTitle;
531+
}
532+
533+
get colorPaletteDialogOKButton() {
534+
return this.moreColorsFeature?.colorPaletteDialogOKButton;
535+
}
536+
537+
get colorPaletteCancelButton() {
538+
return this.moreColorsFeature?.colorPaletteCancelButton;
539+
}
540+
531541
/**
532542
* Returns the selected color.
533543
*/
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
{{#if _showMoreColors}}
2-
<ui5-dialog
3-
header-text="{{moreColorsFeature.colorPaletteDialogTitle}}"
4-
>
5-
<div class="ui5-cp-dialog-content">
6-
<ui5-color-picker></ui5-color-picker>
7-
</div>
1+
{{#if _effectiveShowMoreColors}}
2+
<ui5-dialog
3+
header-text="{{colorPaletteDialogTitle}}"
4+
>
5+
<div class="ui5-cp-dialog-content">
6+
<ui5-color-picker></ui5-color-picker>
7+
</div>
88

9-
<div slot="footer" class="ui5-cp-dialog-footer">
10-
<ui5-button
11-
design="Emphasized"
12-
@click="{{_chooseCustomColor}}"
13-
>{{moreColorsFeature.colorPaletteDialogOKButton}}</ui5-button>
14-
<ui5-button
15-
design="Transparent"
16-
@click="{{_closeDialog}}"
17-
>{{moreColorsFeature.colorPaletteCancelButton}}</ui5-button>
18-
</div>
19-
</ui5-dialog>
9+
<div slot="footer" class="ui5-cp-dialog-footer">
10+
<ui5-button
11+
design="Emphasized"
12+
@click="{{_chooseCustomColor}}"
13+
>{{colorPaletteDialogOKButton}}</ui5-button>
14+
<ui5-button
15+
design="Transparent"
16+
@click="{{_closeDialog}}"
17+
>{{colorPaletteCancelButton}}</ui5-button>
18+
</div>
19+
</ui5-dialog>
2020
{{/if}}

packages/main/src/Input.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
{{> postContent }}
6262

63-
{{#if showSuggestions}}
63+
{{#if _effectiveShowSuggestions}}
6464
<span id="suggestionsText" class="ui5-hidden-text">{{suggestionsText}}</span>
6565
<span id="selectionText" class="ui5-hidden-text" aria-live="polite" role="status"></span>
6666
<span id="suggestionsCount" class="ui5-hidden-text" aria-live="polite">{{availableSuggestionsCount}}</span>

0 commit comments

Comments
 (0)