Skip to content

Commit

Permalink
feat(framework): Add dynamic language change and on-demand rerendering (
Browse files Browse the repository at this point in the history
  • Loading branch information
vladitasev committed Jun 11, 2020
1 parent 33fa055 commit 1b568f2
Show file tree
Hide file tree
Showing 38 changed files with 269 additions and 71 deletions.
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

0 comments on commit 1b568f2

Please sign in to comment.