Skip to content

Commit

Permalink
Rework lit-localize-status event to convey loading/ready/error state
Browse files Browse the repository at this point in the history
  • Loading branch information
aomarks committed Aug 12, 2020
1 parent c214df3 commit 38dc4bd
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 62 deletions.
71 changes: 62 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ bundle. For example:
const {getLocale} = {getLocale: () => 'es-419'};
```

### `getLocale(): string`
### `getLocale() => string`

Return the active locale code.

### `setLocale(locale: string): Promise`
### `setLocale(locale: string) => Promise`

Set the active locale code, and begin loading templates for that locale using
the `loadLocale` function that was passed to `configureLocalization`. Returns a
Expand Down Expand Up @@ -156,21 +156,74 @@ template for each emitted locale. For example:
html`Hola <b>${getUsername()}!</b>`;
```

### `LOCALE_CHANGED_EVENT: string`
### `LOCALE_STATUS_EVENT`

Whenever the locale changes and templates have finished loading, an event by
this name (`"lit-localize-locale-changed"`) is dispatched to `window`.
Name of the [`lit-localize-status` event](#lit-localize-status-event).

## `lit-localize-status` event

In runtime mode, whenever a locale change starts, finishes successfully, or
fails, lit-localize will dispatch a `lit-localize-status` event to `window`.

You can listen for this event to know when your application should be
re-rendered following a locale change. See also the
[`Localized`](#localized-mixin) mixin, which automatically re-renders
`LitElement` classes using this event.

```typescript
import {LOCALE_CHANGED_EVENT} from 'lit-localize';
### Event types

The `detail.status` string property tells you what kind of status change has occured,
and can be one of: `loading`, `ready`, or `error`:

#### `loading`

A new locale has started to load. The `detail` object also contains:

- `loadingLocale: string`: Code of the locale that has started loading.

A `loading` status can be followed by a `ready`, `error`, or `loading` status.
It will be followed by another `loading` status in the case that a second locale
was requested before the first one finished loading.

#### `ready`

A new locale has successfully loaded and is ready for rendering. The `detail` object also contains:

window.addEventListener(LOCALE_CHANGED_EVENT, () => {
renderApplication();
- `readyLocale: string`: Code of the locale that has successfully loaded.

A `ready` status can be followed only by a `loading` status.

#### `error`

A new locale failed to load. The `detail` object also contains the following
properties:

- `errorLocale: string`: Code of the locale that failed to load.
- `errorMessage: string`: Error message from locale load failure.

An `error` status can be followed only by a `loading` status.

### Event example

```typescript
// Show/hide a progress indicator whenever a new locale is loading,
// and re-render the application every time a new locale successfully loads.
window.addEventListener('lit-localize-status', (event) => {
const spinner = document.querySelector('#spinner');
if (event.detail.status === 'loading') {
console.log(`Loading new locale: ${event.detail.loadingLocale}`);
spinner.removeAttribute('hidden');
} else if (event.detail.status === 'ready') {
console.log(`Loaded new locale: ${event.detail.readyLocale}`);
spinner.addAttribute('hidden');
renderApplication();
} else if (event.detail.status === 'error') {
console.error(
`Error loading locale ${event.detail.errorLocale}: ` +
event.detail.errorMessage
);
spinner.addAttribute('hidden');
}
});
```

Expand Down
75 changes: 52 additions & 23 deletions src/outputters/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,13 @@ class Transformer {
}

// import ... from 'lit-localize' -> (removed)
if (this.isLitLocalizeImport(node)) {
return undefined;
if (ts.isImportDeclaration(node)) {
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
node.moduleSpecifier
);
if (moduleSymbol && this.isLitLocalizeModule(moduleSymbol)) {
return undefined;
}
}

if (ts.isCallExpression(node)) {
Expand Down Expand Up @@ -172,17 +177,41 @@ class Transformer {
}
}

// LOCALE_CHANGED_EVENT -> "lit-localize-locale-changed"
// LOCALE_STATUS_EVENT -> "lit-localize-status"
//
// This is slightly odd, but by replacing the LOCALE_CHANGED_EVENT const
// with its static string value, we don't have to be smart about deciding
// when to remove the 'lit-localize' module import, since we can assume that
// everything it exports will be transformed out.
if (
ts.isIdentifier(node) &&
this.typeHasProperty(node, '_LIT_LOCALIZE_LOCALE_CHANGED_EVENT_')
) {
return ts.createStringLiteral('lit-localize-locale-changed');
// We want to replace this imported string constant with its static value so
// that we can always safely remove the 'lit-localize' module import.
//
// TODO(aomarks) Maybe we should error here instead, since lit-localize
// won't fire any of these events in transform mode? But I'm still thinking
// about the use case of an app that can run in either runtime or transform
// mode without code changes (e.g. runtime for dev, transform for
// production)...
//
// We can't tag this string const with a special property like we do with
// our exported functions, because doing so breaks lookups into
// `WindowEventMap`. So we instead identify the symbol by name, and check
// that it was declared in the lit-localize module.
let eventSymbol = this.typeChecker.getSymbolAtLocation(node);
if (eventSymbol && eventSymbol.name === 'LOCALE_STATUS_EVENT') {
if (eventSymbol.flags & ts.SymbolFlags.Alias) {
// Symbols will be aliased in the case of
// `import {LOCALE_STATUS_EVENT} ...`
// but not in the case of `import * as ...`.
eventSymbol = this.typeChecker.getAliasedSymbol(eventSymbol);
}
for (const decl of eventSymbol.declarations) {
let sourceFile: ts.Node = decl;
while (!ts.isSourceFile(sourceFile)) {
sourceFile = sourceFile.parent;
}
const sourceFileSymbol = this.typeChecker.getSymbolAtLocation(
sourceFile
);
if (sourceFileSymbol && this.isLitLocalizeModule(sourceFileSymbol)) {
return ts.createStringLiteral('lit-localize-status');
}
}
}

return ts.visitEachChild(node, this.boundVisitNode, this.context);
Expand Down Expand Up @@ -410,17 +439,11 @@ class Transformer {
}

/**
* Return whether the given node is an import for the lit-localize main
* module, or the localized-element module.
* Return whether the given symbol looks like one of the lit-localize modules
* (because it exports one of the special tagged functions).
*/
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
if (!ts.isImportDeclaration(node)) {
return false;
}
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
node.moduleSpecifier
);
if (!moduleSymbol || !moduleSymbol.exports) {
isLitLocalizeModule(moduleSymbol: ts.Symbol): boolean {
if (!moduleSymbol.exports) {
return false;
}
const exports = moduleSymbol.exports.values();
Expand All @@ -429,7 +452,13 @@ class Transformer {
}) {
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
const props = this.typeChecker.getPropertiesOfType(type);
if (props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_')) {
if (
props.some(
(prop) =>
prop.escapedName === '_LIT_LOCALIZE_MSG_' ||
prop.escapedName === '_LIT_LOCALIZE_LOCALIZED_'
)
) {
return true;
}
}
Expand Down
48 changes: 43 additions & 5 deletions src/tests/transform.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,48 @@ test('configureLocalization() throws', (t) => {
);
});

test('LOCALE_CHANGED_EVENT => "lit-localize-locale-changed"', (t) => {
test('LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
checkTransform(
t,
`import {LOCALE_CHANGED_EVENT} from './lib_client/index.js';
window.addEventListener(LOCALE_CHANGED_EVENT, () => console.log('ok'));`,
`window.addEventListener('lit-localize-locale-changed', () => console.log('ok'));`
`import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
window.addEventListener(LOCALE_STATUS_EVENT, () => console.log('ok'));`,
`window.addEventListener('lit-localize-status', () => console.log('ok'));`
);
});

test('litLocalize.LOCALE_STATUS_EVENT => "lit-localize-status"', (t) => {
checkTransform(
t,
`import * as litLocalize from './lib_client/index.js';
window.addEventListener(litLocalize.LOCALE_STATUS_EVENT, () => console.log('ok'));`,
`window.addEventListener('lit-localize-status', () => console.log('ok'));`
);
});

test('re-assigned LOCALE_STATUS_EVENT', (t) => {
checkTransform(
t,
`import {LOCALE_STATUS_EVENT} from './lib_client/index.js';
const event = LOCALE_STATUS_EVENT;
window.addEventListener(event, () => console.log('ok'));`,
`const event = 'lit-localize-status';
window.addEventListener(event, () => console.log('ok'));`
);
});

test('different LOCALE_STATUS_EVENT variable unchanged', (t) => {
checkTransform(
t,
`const LOCALE_STATUS_EVENT = "x";`,
`const LOCALE_STATUS_EVENT = "x";`
);
});

test('different variable cast to "lit-localie-status" unchanged', (t) => {
checkTransform(
t,
`const x = "x" as "lit-localize-status";`,
`const x = "x";`
);
});

Expand All @@ -380,6 +416,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
t,
`import {LitElement, html} from 'lit-element';
import {Localized} from './lib_client/localized-element.js';
import {msg} from './lib_client/index.js';
class MyElement extends Localized(LitElement) {
render() {
return html\`<b>\${msg('greeting', 'Hello World!')}</b>\`;
Expand All @@ -390,6 +427,7 @@ test('Localized(LitElement) -> LitElement', (t) => {
render() {
return html\`<b>Hello World!</b>\`;
}
}`
}`,
{autoImport: false}
);
});
Loading

0 comments on commit 38dc4bd

Please sign in to comment.