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

feat(framework): Add dynamic language change and on-demand rerendering #1746

Merged
merged 12 commits into from
Jun 11, 2020
15 changes: 13 additions & 2 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ The `theme` setting values above are the technical names of our themes.

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.

This configuration setting should not be used by applications. It is only internally used for specific integration scenarios.
The `RTL` configuration setting should not be used by applications. It is only internally used for specific integration scenarios.

*Note:* Whenever you change `dir` dynamically, make sure you call the `applyDirection` method to re-render the RTL-aware components.

Example:
```js
import applyDirection from "@ui5/webcomponents-base/dist/locale/applyDirection.js";

document.body.dir = "rtl";
applyDirection();
```


<a name="animationMode"></a>
### Animation Mode
Expand Down Expand Up @@ -122,7 +133,7 @@ To do so, please import the desired functionality from the respective `"@ui5/web
import { getTheme, setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
import { getNoConflict, setNoConflict } from "@ui5/webcomponents-base/dist/config/NoConflict.js";
import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js";
import { getLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
import { getLanguage, setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
import { getCalendarType } from "@ui5/webcomponents-base/dist/config/CalendarType.js";
import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js";
```
14 changes: 4 additions & 10 deletions packages/base/src/CustomElementsRegistry.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import setToArray from "./util/setToArray.js";

const Definitions = new Set();
const Failures = new Set();
let failureTimeout;
Expand All @@ -11,11 +13,7 @@ const isTagRegistered = tag => {
};

const getAllRegisteredTags = () => {
const arr = [];
Definitions.forEach(tag => {
arr.push(tag);
});
return arr;
return setToArray(Definitions);
};

const recordTagRegistrationFailure = tag => {
Expand All @@ -29,11 +27,7 @@ const recordTagRegistrationFailure = tag => {
};

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

Expand Down
14 changes: 11 additions & 3 deletions packages/base/src/EventProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@ class EventProvider {
}
}

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

if (!eventListeners) {
return;
return [];
}

eventListeners.forEach(event => {
event["function"].call(this, data); // eslint-disable-line
return eventListeners.map(event => {
return event["function"].call(this, data); // eslint-disable-line
});
}

Expand Down
32 changes: 32 additions & 0 deletions packages/base/src/RenderScheduler.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import RenderQueue from "./RenderQueue.js";
import { getAllRegisteredTags } from "./CustomElementsRegistry.js";
import { isRtlAware } from "./locale/RTLAwareRegistry.js";

const MAX_RERENDER_COUNT = 10;
const registeredElements = new Set();

// Tells whether a render task is currently scheduled
let renderTaskId;
Expand Down Expand Up @@ -141,6 +143,36 @@ class RenderScheduler {
renderTaskPromise = undefined;
}
}

static register(element) {
registeredElements.add(element);
}

static deregister(element) {
registeredElements.delete(element);
}

/**
* Re-renders all UI5 Elements on the page, with the option to specify filters to rerender only some components.
*
* Usage:
* reRenderAllUI5Elements() -> rerenders all components
* reRenderAllUI5Elements({rtlAware: true}) -> re-renders only rtlAware components
* reRenderAllUI5Elements({languageAware: true}) -> re-renders only languageAware components
* reRenderAllUI5Elements({rtlAware: true, languageAware: true}) -> re-renders components that are rtlAware or languageAware
*
* @public
* @param {Object|undefined} filters - Object with keys that can be "rtlAware" or "languageAware"
*/
static reRenderAllUI5Elements(filters) {
registeredElements.forEach(element => {
const rtlAware = isRtlAware(element.constructor);
const languageAware = element.constructor.getMetadata().isLanguageAware();
if (!filters || (filters.rtlAware && rtlAware) || (filters.languageAware && languageAware)) {
RenderScheduler.renderDeferred(element);
}
});
}
}

export default RenderScheduler;
5 changes: 5 additions & 0 deletions packages/base/src/UI5Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Float from "./types/Float.js";
import { kebabToCamelCase, camelToKebabCase } from "./util/StringHelper.js";
import isValidPropertyName from "./util/isValidPropertyName.js";
import isSlot from "./util/isSlot.js";
import { markAsRtlAware } from "./locale/RTLAwareRegistry.js";

const metadata = {
events: {
Expand Down Expand Up @@ -114,6 +115,7 @@ class UI5Element extends HTMLElement {
await Promise.resolve();
}

RenderScheduler.register(this);
await RenderScheduler.renderImmediately(this);
this._domRefReadyPromise._deferredResolve();
if (typeof this.onEnterDOM === "function") {
Expand All @@ -136,6 +138,7 @@ class UI5Element extends HTMLElement {
this._stopObservingDOMChildren();
}

RenderScheduler.deregister(this);
if (typeof this.onExitDOM === "function") {
this.onExitDOM();
}
Expand Down Expand Up @@ -672,6 +675,8 @@ class UI5Element extends HTMLElement {
* @returns {String|undefined}
*/
get effectiveDir() {
markAsRtlAware(this.constructor); // if a UI5 Element calls this method, it's considered to be rtl-aware

const doc = window.document;
const dirValues = ["ltr", "rtl"]; // exclude "auto" and "" from all calculations
const locallyAppliedDir = getComputedStyle(this).getPropertyValue(GLOBAL_DIR_CSS_VAR);
Expand Down
8 changes: 8 additions & 0 deletions packages/base/src/UI5ElementMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ class UI5ElementMetadata {
getEvents() {
return this.metadata.events || {};
}

/**
* Determines whether this UI5 Element has any translatable texts (needs to be invalidated upon language change)
* @returns {boolean}
*/
isLanguageAware() {
return !!this.metadata.languageAware;
}
}

const validateSingleProperty = (value, propData) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/base/src/asset-registries/i18n.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getFeature } from "../FeaturesRegistry.js";
import getLocale from "../locale/getLocale.js";
import { attachLanguageChange } from "../locale/languageChange.js";
import { fetchTextOnce } from "../util/FetchHelper.js";
import normalizeLocale from "../locale/normalizeLocale.js";
import nextFallbackLocale from "../locale/nextFallbackLocale.js";
Expand Down Expand Up @@ -63,6 +64,7 @@ const fetchI18nBundle = async packageName => {
}

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

Expand Down Expand Up @@ -90,6 +92,12 @@ const fetchI18nBundle = async packageName => {
setI18nBundleData(packageName, data);
};

// When the language changes dynamically (the user calls setLanguage), re-fetch all previously fetched bundles
attachLanguageChange(() => {
const allPackages = [...bundleData.keys()];
return Promise.all(allPackages.map(fetchI18nBundle));
});

export {
fetchI18nBundle,
registerI18nBundle,
Expand Down
31 changes: 30 additions & 1 deletion packages/base/src/config/Language.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
import { getLanguage as getConfiguredLanguage } from "../InitialConfiguration.js";
import { fireLanguageChange } from "../locale/languageChange.js";
import RenderScheduler from "../RenderScheduler.js";

let language;

/**
* Returns the currently configured language, or the browser language as a fallback
* @returns {String}
*/
const getLanguage = () => {
if (language === undefined) {
language = getConfiguredLanguage();
}
return language;
};

export { getLanguage }; // eslint-disable-line
/**
* Changes the current language, re-fetches all message bundles, updates all language-aware components
* and returns a promise that resolves when all rendering is done
*
* @param newLanguage
* @returns {Promise<void>}
*/
const setLanguage = async newLanguage => {
if (language === newLanguage) {
return;
}

language = newLanguage;

const listenersResults = fireLanguageChange(newLanguage);
await Promise.all(listenersResults);
RenderScheduler.reRenderAllUI5Elements({ languageAware: true });
return RenderScheduler.whenFinished();
};

export {
getLanguage,
setLanguage,
};
14 changes: 14 additions & 0 deletions packages/base/src/locale/RTLAwareRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const rtlAwareSet = new Set();

const markAsRtlAware = klass => {
rtlAwareSet.add(klass);
};

const isRtlAware = klass => {
return rtlAwareSet.has(klass);
};

export {
markAsRtlAware,
isRtlAware,
};
15 changes: 15 additions & 0 deletions packages/base/src/locale/applyDirection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import RenderScheduler from "../RenderScheduler.js";

/**
* Re-renders all RTL-aware UI5 Elements.
* Call this method whenever you change the "dir" property anywhere in your HTML page
* Example: document.body.dir = "rtl"; applyDirection();
*
* @returns {Promise<void>}
*/
const applyDirection = () => {
RenderScheduler.reRenderAllUI5Elements({ rtlAware: true });
return RenderScheduler.whenFinished();
};

export default applyDirection;
22 changes: 22 additions & 0 deletions packages/base/src/locale/languageChange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import EventProvider from "../EventProvider.js";

const eventProvider = new EventProvider();
const LANG_CHANGE = "languageChange";

const attachLanguageChange = listener => {
eventProvider.attachEvent(LANG_CHANGE, listener);
};

const detachLanguageChange = listener => {
eventProvider.detachEvent(LANG_CHANGE, listener);
};

const fireLanguageChange = lang => {
return eventProvider.fireEvent(LANG_CHANGE, lang);
};

export {
attachLanguageChange,
detachLanguageChange,
fireLanguageChange,
};
10 changes: 10 additions & 0 deletions packages/base/src/util/setToArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This is needed as IE11 doesn't have Set.prototype.keys/values/entries, so [...mySet.values()] is not an option
const setToArray = s => {
const arr = [];
s.forEach(item => {
arr.push(item);
});
return arr;
};

export default setToArray;
1 change: 0 additions & 1 deletion packages/fiori/src/NotificationListGroupItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import NotificationListGroupItemCss from "./generated/themes/NotificationListGro
*/
const metadata = {
tag: "ui5-li-notification-group",
rtlAware: true,
languageAware: true,
managedSlots: true,
properties: /** @lends sap.ui.webcomponents.fiori.NotificationListGroupItem.prototype */ {
Expand Down
1 change: 0 additions & 1 deletion packages/fiori/src/NotificationListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const MAX_WRAP_HEIGHT = 32; // px.
*/
const metadata = {
tag: "ui5-li-notification",
rtlAware: true,
languageAware: true,
managedSlots: true,
properties: /** @lends sap.ui.webcomponents.fiori.NotificationListItem.prototype */ {
Expand Down
1 change: 0 additions & 1 deletion packages/fiori/src/ShellBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import styles from "./generated/themes/ShellBar.css.js";
*/
const metadata = {
tag: "ui5-shellbar",
rtlAware: true,
languageAware: true,
properties: /** @lends sap.ui.webcomponents.fiori.ShellBar.prototype */ {

Expand Down
7 changes: 6 additions & 1 deletion packages/main/bundle.es5.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import "./bundle.esm.js";

import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js";
import { getTheme, setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
import { getLanguage, setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
import { setNoConflict } from "@ui5/webcomponents-base/dist/config/NoConflict.js";
import { getRTL } from "@ui5/webcomponents-base/dist/config/RTL.js";
import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js";
import { getRegisteredNames as getIconNames } from "@ui5/webcomponents-base/dist/SVGIconRegistry.js"
import { getRegisteredNames as getIconNames } from "@ui5/webcomponents-base/dist/SVGIconRegistry.js";
import applyDirection from "@ui5/webcomponents-base/dist/locale/applyDirection.js";
const configuration = {
getAnimationMode,
getTheme,
setTheme,
getLanguage,
setLanguage,
setNoConflict,
getRTL,
getFirstDayOfWeek,
};
export {
configuration,
getIconNames,
applyDirection,
};
7 changes: 6 additions & 1 deletion packages/main/bundle.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,24 @@ window.isIE = isIE; // attached to the window object for testing purposes
// Note: keep in sync with rollup.config value for IIFE
import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js";
import { getTheme, setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
import { getLanguage, setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
import { setNoConflict } from "@ui5/webcomponents-base/dist/config/NoConflict.js";
import { getRTL } from "@ui5/webcomponents-base/dist/config/RTL.js";
import { getFirstDayOfWeek } from "@ui5/webcomponents-base/dist/config/FormatSettings.js";
import { getRegisteredNames as getIconNames } from "@ui5/webcomponents-base/dist/SVGIconRegistry.js"
import { getRegisteredNames as getIconNames } from "@ui5/webcomponents-base/dist/SVGIconRegistry.js";
import applyDirection from "@ui5/webcomponents-base/dist/locale/applyDirection.js";
window["sap-ui-webcomponents-bundle"] = {
configuration : {
getAnimationMode,
getTheme,
setTheme,
getLanguage,
setLanguage,
setNoConflict,
getRTL,
getFirstDayOfWeek,
},
getIconNames,
getLocaleData,
applyDirection,
};
Loading