Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/opensource/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
{
"glob": "**/*",
"input": "apps/opensource/public"
},
{
"input": "apps/opensource/src/lazy-loaded/lib-1/i18n/assets/",
"glob": "*.json",
"output": "assets/i18n/lazy-loaded/lib-1"
},
{
"input": "apps/opensource/src/lazy-loaded/lib-2/i18n/assets/",
"glob": "*.json",
"output": "assets/i18n/lazy-loaded/lib-2"
}
],
"styles": ["apps/opensource/src/styles.scss"]
Expand Down
23 changes: 23 additions & 0 deletions apps/opensource/src/app/app.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<ngx-toast-container />

@if (navigating()) {
<div class="nav-loading-indicator" role="status" aria-live="polite">Loading…</div>
}

<h1>{{'title' | translate}}</h1>

<ng-container *ngxMediaQuery="['Desktop', 'Tablet']">
Expand Down Expand Up @@ -29,4 +33,23 @@ <h1>{{'title' | translate}}</h1>

{{['Desktop', 'DesktopWide', 'Tablet'] | ngxMatchesQuery: 'some'}}

<nav>
<ul>
<li>
<a
[routerLink]="navigating() ? null : ['/', i18nService.currentLanguage, 'lib-1']"
[attr.aria-disabled]="navigating()"
>Move to lazy loaded lib-1</a
>
</li>
<li>
<a
[routerLink]="navigating() ? null : ['/', i18nService.currentLanguage, 'lib-2']"
[attr.aria-disabled]="navigating()"
>Move to lazy loaded lib-2</a
>
</li>
</ul>
</nav>

<router-outlet></router-outlet>
10 changes: 10 additions & 0 deletions apps/opensource/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export const appRoutes: Route[] = [
},
['i18n/layout/']
),
{
path: 'lib-1',
loadChildren: () =>
import('../lazy-loaded/lib-1/lib-1.routes').then((m) => m.LazyLoadedLib1Routes),
},
{
path: 'lib-2',
loadChildren: () =>
import('../lazy-loaded/lib-2/lib-2.routes').then((m) => m.LazyLoadedLib2Routes),
},
],
},
];
11 changes: 11 additions & 0 deletions apps/opensource/src/app/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.nav-loading-indicator {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
color: #555;
}

nav a[aria-disabled='true'] {
pointer-events: none;
opacity: 0.5;
cursor: default;
}
37 changes: 33 additions & 4 deletions apps/opensource/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { Component, inject, signal, WritableSignal } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { Component, inject, Signal, signal, WritableSignal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
NavigationCancel,
NavigationEnd,
NavigationError,
NavigationStart,
Router,
RouterModule,
} from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { from, of, switchMap } from 'rxjs';
import { filter, from, map, of, switchMap } from 'rxjs';

import { NgxI18nService } from '@lib/ngx-i18n';
import {
NgxModalService,
NgxToastContainerComponent,
Expand Down Expand Up @@ -42,10 +51,30 @@ export class App {
private readonly modalService = inject(NgxModalService);
private readonly router: Router = inject(Router);
private readonly mediaQueryService = inject(NgxMediaQueryService);
protected readonly i18nService: NgxI18nService = inject(NgxI18nService);
private toastAmount: number = 1;

public loading: WritableSignal<boolean> = signal(false);

/**
* Guards/resolvers on a route (e.g. the lib-2 start resolver) can take a while to settle.
* Angular keeps the previously activated route rendered in the meantime, so without this
* indicator that wait is invisible and a click during it just cancels the navigation.
*/
public readonly navigating: Signal<boolean> = toSignal(
this.router.events.pipe(
filter(
(event) =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError
),
map((event) => event instanceof NavigationStart)
),
{ initialValue: false }
);

constructor() {
this.mediaQueryService.currentQueryMatch$.subscribe(console.log);
}
Expand All @@ -69,7 +98,7 @@ export class App {
content: 'This is where we have the display content directive!',
tourItem: 'display-content',
beforeVisible: () => {
return from(this.router.navigate(['../../en','layout']));
return from(this.router.navigate(['../../en', 'layout']));
},
},
]);
Expand Down
7 changes: 7 additions & 0 deletions apps/opensource/src/lazy-loaded/lib-1/i18n/assets/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"lib-1": {
"start": {
"body": "<p>AngularJS (also known as Angular 1) is a discontinued free and open-source JavaScript-based web framework for developing single-page applications. It was maintained mainly by Google and a community of individuals and corporations. It aimed to simplify both the development and the testing of such applications by providing a framework for client-side model–view–controller (MVC) and model–view–viewmodel (MVVM) architectures, along with components commonly used in web applications and progressive web applications.</p><p>AngularJS was used as the frontend of the MEAN stack, that consisted of MongoDB database, Express.js web application server framework, AngularJS itself (or Angular), and Node.js server runtime environment.</p><p>As of January 1, 2022, Google no longer updates AngularJS to fix security, browser compatibility, or jQuery issues. The Angular team recommends upgrading to Angular (v2+) as the best path forward, but they also provided some other options.</p?"
}
}
}
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-1/i18n/const/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TRANSLATION_PATHS = ['./assets/i18n/lazy-loaded/lib-1/'];
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-1/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './const';
Empty file.
16 changes: 16 additions & 0 deletions apps/opensource/src/lazy-loaded/lib-1/lib-1.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Routes } from '@angular/router';

import { provideWithTranslations } from '@lib/ngx-i18n';

import { TRANSLATION_PATHS } from './i18n';
import { Lib1StartComponent } from './pages';

export const LazyLoadedLib1Routes: Routes = [
provideWithTranslations(
{
path: '',
component: Lib1StartComponent,
},
TRANSLATION_PATHS
),
];
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-1/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Lib1StartComponent } from './lib-1-start/lib-1-start.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div [innerHTML]="'lib-1.start.body' | translate"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';

@Component({
selector: 'lib-1-start-page',
templateUrl: './lib-1-start.component.html',
styleUrl: './lib-1-start.component.scss',
imports: [TranslatePipe],
})
export class Lib1StartComponent {}
7 changes: 7 additions & 0 deletions apps/opensource/src/lazy-loaded/lib-2/i18n/assets/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"lib-2": {
"start": {
"body": "<p>Angular (also referred to as Angular 2+) is a TypeScript-based free and open-source single-page web application framework. It is developed by Google and by a community of individuals and corporations. Angular is a complete rewrite from the same team that built AngularJS. The Angular ecosystem consists of a diverse group of over 1.7 million developers, library authors, and content creators. According to the Stack Overflow Developer Survey, Angular is one of the most commonly used web frameworks.</p><p>Google designed Angular as a ground-up rewrite of AngularJS. Unlike AngularJS, Angular does not have a concept of \"scope\" or controllers; instead, it uses a hierarchy of components as its primary architectural characteristic.[8] Angular has a different expression syntax, focusing on \"[ ]\" for property binding, and \"( )\" for event binding. Angular recommends the use of Microsoft's TypeScript language, which introduces features such as static typing, generics, and type annotations.</p>"
}
}
}
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-2/i18n/const/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TRANSLATION_PATHS = ['./assets/i18n/lazy-loaded/lib-2/'];
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-2/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './const';
Empty file.
20 changes: 20 additions & 0 deletions apps/opensource/src/lazy-loaded/lib-2/lib-2.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Routes } from '@angular/router';

import { provideWithTranslations } from '@lib/ngx-i18n';

import { TRANSLATION_PATHS } from './i18n';
import { Lib2StartComponent } from './pages';
import { startResolver } from './resolvers';

export const LazyLoadedLib2Routes: Routes = [
provideWithTranslations(
{
path: '',
component: Lib2StartComponent,
resolve: {
data: startResolver,
},
},
TRANSLATION_PATHS
),
];
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-2/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Lib2StartComponent } from './lib-2-start/lib-2-start.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div [innerHTML]="'lib-2.start.body' | translate"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';

@Component({
selector: 'lib-2-start-page',
templateUrl: './lib-2-start.component.html',
styleUrl: './lib-2-start.component.scss',
imports: [TranslatePipe],
})
export class Lib2StartComponent {}
1 change: 1 addition & 0 deletions apps/opensource/src/lazy-loaded/lib-2/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { startResolver } from './start/start.resolver';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ResolveFn } from '@angular/router';
import { delay, Observable, of } from 'rxjs';

export const startResolver: ResolveFn<Observable<boolean>> = (): Observable<boolean> => {
return of(true).pipe(delay(1000));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { HttpBackend } from '@angular/common/http';
import { Injector, runInInjectionContext } from '@angular/core';
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { of, throwError } from 'rxjs';

import { NgxI18nLoadingService } from '../../services';
import {
NgxI18nClientToken,
NgxI18nConfigurationToken,
NgxI18nTranslationPathsToken,
} from '../../tokens';

import { NgxI18nMultiTranslationHttpLoader } from './multi-translation.loader';

describe('NgxI18nMultiTranslationHttpLoader', () => {
const createLoader = (
loadingService: NgxI18nLoadingService,
client: { getTranslations: jest.Mock },
paths: string[]
): NgxI18nMultiTranslationHttpLoader => {
const injector = Injector.create({
providers: [
{ provide: NgxI18nLoadingService, useValue: loadingService },
{ provide: NgxI18nClientToken, useValue: client },
{ provide: NgxI18nConfigurationToken, useValue: {} },
{ provide: HttpBackend, useValue: {} },
{ provide: NgxI18nTranslationPathsToken, useValue: paths },
],
});

return runInInjectionContext(injector, () => new NgxI18nMultiTranslationHttpLoader());
};

describe('getTranslation', () => {
it('should not persist a failed fetch to the translations store, so it can be retried when the next loader is called that has overlap for that path.', () => {
const failingPath = './assets/i18n/failing-path/';
const translations = { foo: 'bar' };

/**
* Denis: NgxI18nLoadingService is providedIn: 'root'. So in an actual app, a single instance
* is shared between the root-scoped loader and every lazy route's own loader (created via
* provideWithTranslations), which is what makes this cross-loader issues possible.
*/
const loadingService = new NgxI18nLoadingService();

const failingClient = {
getTranslations: jest
.fn()
.mockReturnValue(throwError(() => new Error('network error'))),
};
const firstLoader = createLoader(loadingService, failingClient, [failingPath]);

subscribeSpyTo(firstLoader.getTranslation('nl'));

// Denis: the failed path should not be cached as "loaded".
expect(loadingService.getTranslations()[failingPath]).toBeUndefined();

const recoveredClient = {
getTranslations: jest.fn().mockReturnValue(of(translations)),
};
const secondLoader = createLoader(loadingService, recoveredClient, [
failingPath,
'./assets/i18n/other-path/',
]);

const secondSpy = subscribeSpyTo(secondLoader.getTranslation('nl'));

expect(recoveredClient.getTranslations).toHaveBeenCalledWith(
failingPath,
'nl',
undefined
);
expect(secondSpy.getFirstValue()).toEqual(translations);
});

it('should persist a successful fetch to the translations store so it is reused by a later loader for an overlapping path.', () => {
const path = './assets/i18n/working-path/';
const translations = { foo: 'bar' };

const loadingService = new NgxI18nLoadingService();

const client = {
getTranslations: jest.fn().mockReturnValue(of(translations)),
};
const firstLoader = createLoader(loadingService, client, [path]);

subscribeSpyTo(firstLoader.getTranslation('nl'));

expect(loadingService.getTranslations()[path]).toEqual(translations);

const secondClient = {
getTranslations: jest.fn().mockReturnValue(of({})),
};
const secondLoader = createLoader(loadingService, secondClient, [
path,
'./assets/i18n/other-path/',
]);

subscribeSpyTo(secondLoader.getTranslation('nl'));

// Denis: the already-loaded path should come from the store, not a fresh fetch.
expect(secondClient.getTranslations).not.toHaveBeenCalledWith(path, 'nl', undefined);
});
});
});
Loading