Skip to content

Commit 1b568f2

Browse files
authored
feat(framework): Add dynamic language change and on-demand rerendering (#1746)
1 parent 33fa055 commit 1b568f2

38 files changed

+269
-71
lines changed

docs/Configuration.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,18 @@ The `theme` setting values above are the technical names of our themes.
3434

3535
In order to have RTL mode, just set the HTML attribute `dir` to `rtl` on the `body`, `html` or any other relevant region of your application.
3636

37-
This configuration setting should not be used by applications. It is only internally used for specific integration scenarios.
37+
The `RTL` configuration setting should not be used by applications. It is only internally used for specific integration scenarios.
38+
39+
*Note:* Whenever you change `dir` dynamically, make sure you call the `applyDirection` method to re-render the RTL-aware components.
40+
41+
Example:
42+
```js
43+
import applyDirection from "@ui5/webcomponents-base/dist/locale/applyDirection.js";
44+
45+
document.body.dir = "rtl";
46+
applyDirection();
47+
```
48+
3849

3950
<a name="animationMode"></a>
4051
### Animation Mode
@@ -122,7 +133,7 @@ To do so, please import the desired functionality from the respective `"@ui5/web
122133
import { getTheme, setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
123134
import { getNoConflict, setNoConflict } from "@ui5/webcomponents-base/dist/config/NoConflict.js";
124135
import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js";
125-
import { getLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
136+
import { getLanguage, setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
126137
import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js";
127138
import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js";
128139
```

packages/base/src/CustomElementsRegistry.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import setToArray from "./util/setToArray.js";
2+
13
const Definitions = new Set();
24
const Failures = new Set();
35
let failureTimeout;
@@ -11,11 +13,7 @@ const isTagRegistered = tag => {
1113
};
1214

1315
const getAllRegisteredTags = () => {
14-
const arr = [];
15-
Definitions.forEach(tag => {
16-
arr.push(tag);
17-
});
18-
return arr;
16+
return setToArray(Definitions);
1917
};
2018

2119
const recordTagRegistrationFailure = tag => {
@@ -29,11 +27,7 @@ const recordTagRegistrationFailure = tag => {
2927
};
3028

3129
const displayFailedRegistrations = () => {
32-
const tags = []; // IE only supports Set.prototype.forEach
33-
Failures.forEach(tag => {
34-
tags.push(tag);
35-
});
36-
console.warn(`The following tags have already been defined by a different UI5 Web Components version: ${tags.join(", ")}`); // eslint-disable-line
30+
console.warn(`The following tags have already been defined by a different UI5 Web Components version: ${setToArray(Failures).join(", ")}`); // eslint-disable-line
3731
Failures.clear();
3832
};
3933

packages/base/src/EventProvider.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,24 @@ class EventProvider {
3434
}
3535
}
3636

37+
/**
38+
* Fires an event and returns the results of all event listeners as an array.
39+
* Example: If listeners return promises, you can: await fireEvent("myEvent") to know when all listeners have finished.
40+
*
41+
* @param eventName the event to fire
42+
* @param data optional data to pass to each event listener
43+
* @returns {Array} an array with the results of all event listeners
44+
*/
3745
fireEvent(eventName, data) {
3846
const eventRegistry = this._eventRegistry;
3947
const eventListeners = eventRegistry[eventName];
4048

4149
if (!eventListeners) {
42-
return;
50+
return [];
4351
}
4452

45-
eventListeners.forEach(event => {
46-
event["function"].call(this, data); // eslint-disable-line
53+
return eventListeners.map(event => {
54+
return event["function"].call(this, data); // eslint-disable-line
4755
});
4856
}
4957

packages/base/src/RenderScheduler.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import RenderQueue from "./RenderQueue.js";
22
import { getAllRegisteredTags } from "./CustomElementsRegistry.js";
3+
import { isRtlAware } from "./locale/RTLAwareRegistry.js";
34

45
const MAX_RERENDER_COUNT = 10;
6+
const registeredElements = new Set();
57

68
// Tells whether a render task is currently scheduled
79
let renderTaskId;
@@ -141,6 +143,36 @@ class RenderScheduler {
141143
renderTaskPromise = undefined;
142144
}
143145
}
146+
147+
static register(element) {
148+
registeredElements.add(element);
149+
}
150+
151+
static deregister(element) {
152+
registeredElements.delete(element);
153+
}
154+
155+
/**
156+
* Re-renders all UI5 Elements on the page, with the option to specify filters to rerender only some components.
157+
*
158+
* Usage:
159+
* reRenderAllUI5Elements() -> rerenders all components
160+
* reRenderAllUI5Elements({rtlAware: true}) -> re-renders only rtlAware components
161+
* reRenderAllUI5Elements({languageAware: true}) -> re-renders only languageAware components
162+
* reRenderAllUI5Elements({rtlAware: true, languageAware: true}) -> re-renders components that are rtlAware or languageAware
163+
*
164+
* @public
165+
* @param {Object|undefined} filters - Object with keys that can be "rtlAware" or "languageAware"
166+
*/
167+
static reRenderAllUI5Elements(filters) {
168+
registeredElements.forEach(element => {
169+
const rtlAware = isRtlAware(element.constructor);
170+
const languageAware = element.constructor.getMetadata().isLanguageAware();
171+
if (!filters || (filters.rtlAware && rtlAware) || (filters.languageAware && languageAware)) {
172+
RenderScheduler.renderDeferred(element);
173+
}
174+
});
175+
}
144176
}
145177

146178
export default RenderScheduler;

packages/base/src/UI5Element.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Float from "./types/Float.js";
1515
import { kebabToCamelCase, camelToKebabCase } from "./util/StringHelper.js";
1616
import isValidPropertyName from "./util/isValidPropertyName.js";
1717
import isSlot from "./util/isSlot.js";
18+
import { markAsRtlAware } from "./locale/RTLAwareRegistry.js";
1819

1920
const metadata = {
2021
events: {
@@ -114,6 +115,7 @@ class UI5Element extends HTMLElement {
114115
await Promise.resolve();
115116
}
116117

118+
RenderScheduler.register(this);
117119
await RenderScheduler.renderImmediately(this);
118120
this._domRefReadyPromise._deferredResolve();
119121
if (typeof this.onEnterDOM === "function") {
@@ -136,6 +138,7 @@ class UI5Element extends HTMLElement {
136138
this._stopObservingDOMChildren();
137139
}
138140

141+
RenderScheduler.deregister(this);
139142
if (typeof this.onExitDOM === "function") {
140143
this.onExitDOM();
141144
}
@@ -672,6 +675,8 @@ class UI5Element extends HTMLElement {
672675
* @returns {String|undefined}
673676
*/
674677
get effectiveDir() {
678+
markAsRtlAware(this.constructor); // if a UI5 Element calls this method, it's considered to be rtl-aware
679+
675680
const doc = window.document;
676681
const dirValues = ["ltr", "rtl"]; // exclude "auto" and "" from all calculations
677682
const locallyAppliedDir = getComputedStyle(this).getPropertyValue(GLOBAL_DIR_CSS_VAR);

packages/base/src/UI5ElementMetadata.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ class UI5ElementMetadata {
126126
getEvents() {
127127
return this.metadata.events || {};
128128
}
129+
130+
/**
131+
* Determines whether this UI5 Element has any translatable texts (needs to be invalidated upon language change)
132+
* @returns {boolean}
133+
*/
134+
isLanguageAware() {
135+
return !!this.metadata.languageAware;
136+
}
129137
}
130138

131139
const validateSingleProperty = (value, propData) => {

packages/base/src/asset-registries/i18n.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getFeature } from "../FeaturesRegistry.js";
22
import getLocale from "../locale/getLocale.js";
3+
import { attachLanguageChange } from "../locale/languageChange.js";
34
import { fetchTextOnce } from "../util/FetchHelper.js";
45
import normalizeLocale from "../locale/normalizeLocale.js";
56
import nextFallbackLocale from "../locale/nextFallbackLocale.js";
@@ -63,6 +64,7 @@ const fetchI18nBundle = async packageName => {
6364
}
6465

6566
if (!bundlesForPackage[localeId]) {
67+
setI18nBundleData(packageName, null); // reset for the default language (if data was set for a previous language)
6668
return;
6769
}
6870

@@ -90,6 +92,12 @@ const fetchI18nBundle = async packageName => {
9092
setI18nBundleData(packageName, data);
9193
};
9294

95+
// When the language changes dynamically (the user calls setLanguage), re-fetch all previously fetched bundles
96+
attachLanguageChange(() => {
97+
const allPackages = [...bundleData.keys()];
98+
return Promise.all(allPackages.map(fetchI18nBundle));
99+
});
100+
93101
export {
94102
fetchI18nBundle,
95103
registerI18nBundle,

packages/base/src/config/Language.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
import { getLanguage as getConfiguredLanguage } from "../InitialConfiguration.js";
2+
import { fireLanguageChange } from "../locale/languageChange.js";
3+
import RenderScheduler from "../RenderScheduler.js";
24

35
let language;
46

7+
/**
8+
* Returns the currently configured language, or the browser language as a fallback
9+
* @returns {String}
10+
*/
511
const getLanguage = () => {
612
if (language === undefined) {
713
language = getConfiguredLanguage();
814
}
915
return language;
1016
};
1117

12-
export { getLanguage }; // eslint-disable-line
18+
/**
19+
* Changes the current language, re-fetches all message bundles, updates all language-aware components
20+
* and returns a promise that resolves when all rendering is done
21+
*
22+
* @param newLanguage
23+
* @returns {Promise<void>}
24+
*/
25+
const setLanguage = async newLanguage => {
26+
if (language === newLanguage) {
27+
return;
28+
}
29+
30+
language = newLanguage;
31+
32+
const listenersResults = fireLanguageChange(newLanguage);
33+
await Promise.all(listenersResults);
34+
RenderScheduler.reRenderAllUI5Elements({ languageAware: true });
35+
return RenderScheduler.whenFinished();
36+
};
37+
38+
export {
39+
getLanguage,
40+
setLanguage,
41+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const rtlAwareSet = new Set();
2+
3+
const markAsRtlAware = klass => {
4+
rtlAwareSet.add(klass);
5+
};
6+
7+
const isRtlAware = klass => {
8+
return rtlAwareSet.has(klass);
9+
};
10+
11+
export {
12+
markAsRtlAware,
13+
isRtlAware,
14+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import RenderScheduler from "../RenderScheduler.js";
2+
3+
/**
4+
* Re-renders all RTL-aware UI5 Elements.
5+
* Call this method whenever you change the "dir" property anywhere in your HTML page
6+
* Example: document.body.dir = "rtl"; applyDirection();
7+
*
8+
* @returns {Promise<void>}
9+
*/
10+
const applyDirection = () => {
11+
RenderScheduler.reRenderAllUI5Elements({ rtlAware: true });
12+
return RenderScheduler.whenFinished();
13+
};
14+
15+
export default applyDirection;

0 commit comments

Comments
 (0)