Skip to content

feat: route config loading #95

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

Merged
merged 8 commits into from
Feb 28, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { EnvironmentVariablesService, provideEnvironmentVariables } from '@infinum/ngx-nuts-and-bolts/env';
import { EnvironmentVariable } from '@ngx-nuts-and-bolts/environment-variables-example-app-base';
import { EnvironmentVariable, envExampleAppRoutes } from '@ngx-nuts-and-bolts/environment-variables-example-app-base';

function appConfig(env: Record<EnvironmentVariable, string>): ApplicationConfig {
return {
providers: [
provideEnvironmentVariables(env),
provideRouter(envExampleAppRoutes),
{
provide: APP_INITIALIZER,
multi: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideUniversalEnvironmentVariables } from '@infinum/ngx-nuts-and-bolts-ssr';
import { EnvironmentVariablesService } from '@infinum/ngx-nuts-and-bolts/env';
import { EnvironmentVariable } from '@ngx-nuts-and-bolts/environment-variables-example-app-base';
import { EnvironmentVariable, envExampleAppRoutes } from '@ngx-nuts-and-bolts/environment-variables-example-app-base';

export const appConfig: ApplicationConfig = {
providers: [
provideUniversalEnvironmentVariables({
publicVariables: [EnvironmentVariable.Foo],
privateVariables: [EnvironmentVariable.Bar], // Value for `Bar` will be `undefined` in the browser, but preset on the server.
}),
provideRouter(envExampleAppRoutes),
{
provide: APP_INITIALIZER,
multi: true,
Expand Down
2 changes: 1 addition & 1 deletion apps/environment-variables-ssr-example/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<ngx-root></ngx-root>
<inf-root></inf-root>
</body>
</html>
3 changes: 2 additions & 1 deletion libs/environment-variables-example-app-base/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/variable-selection.component';
export * from './lib/enums/environment-variable.enum';
export * from './lib/routes';
export * from './lib/variable-selection.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
selector: 'inf-nuts-and-bolts-bar-page',
template: 'Bar',
})
export class BarPageComponent {}

@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: BarPageComponent,
},
]),
],
})
export class BarPageModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Component, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

@Component({
selector: 'inf-nuts-and-bolts-foo-page',
template: 'Foo',
})
export class FooPageComponent {}

@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: FooPageComponent,
},
]),
],
})
export class FooPageModule {}
12 changes: 12 additions & 0 deletions libs/environment-variables-example-app-base/src/lib/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Route } from '@angular/router';

export const envExampleAppRoutes: Array<Route> = [
{
path: 'foo',
loadChildren: () => import('./pages/foo.page').then((m) => m.FooPageModule),
},
{
path: 'bar',
loadChildren: () => import('./pages/bar.page').then((m) => m.BarPageModule),
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,22 @@
</label>

<p>Value: {{ selectedVariable | environmentVariableValue }}</p>

<hr />

<p>For testing global loader during chunk loading (<em>throttle network to see it</em>):</p>

<nav>
<ul>
<li>
<a routerLink="foo">Foo page</a>
</li>
<li>
<a routerLink="bar">Bar page</a>
</li>
</ul>
</nav>

<p>Is route loading: {{ isRouteConfigLoading$ | async }}</p>

<router-outlet></router-outlet>
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* istanbul ignore file */
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterModule } from '@angular/router';
import { EnvironmentVariablesService } from '@infinum/ngx-nuts-and-bolts/env';
import { createRouteConfigLoadingObservable } from '@infinum/ngx-nuts-and-bolts/routing-utils';
import { EnvironmentVariable } from './enums/environment-variable.enum';
import { EnvironmentVariableValuePipe } from './pipes/environment-variable-value/environment-variable-value.pipe';

Expand All @@ -11,13 +13,16 @@ import { EnvironmentVariableValuePipe } from './pipes/environment-variable-value
templateUrl: './variable-selection.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CommonModule, EnvironmentVariableValuePipe, FormsModule],
imports: [CommonModule, EnvironmentVariableValuePipe, FormsModule, RouterModule],
})
export class VariableSelectionComponent {
public readonly variables = Object.entries(EnvironmentVariable);
public selectedVariable = EnvironmentVariable.Foo;
private readonly router = inject(Router);
public readonly isRouteConfigLoading$ = createRouteConfigLoadingObservable(this.router);
private readonly env = inject(EnvironmentVariablesService<EnvironmentVariable>);

constructor(private readonly env: EnvironmentVariablesService<EnvironmentVariable>) {
constructor() {
for (const variableName of Object.values(EnvironmentVariable)) {
console.log(variableName, this.env.get(variableName));
}
Expand Down
4 changes: 3 additions & 1 deletion libs/ngx-nuts-and-bolts/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"libs/ngx-nuts-and-bolts/form-utils/**/*.ts",
"libs/ngx-nuts-and-bolts/form-utils/**/*.html",
"libs/ngx-nuts-and-bolts/utility-types/**/*.ts",
"libs/ngx-nuts-and-bolts/utility-types/**/*.html"
"libs/ngx-nuts-and-bolts/utility-types/**/*.html",
"libs/ngx-nuts-and-bolts/routing-utils/**/*.ts",
"libs/ngx-nuts-and-bolts/routing-utils/**/*.html"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions libs/ngx-nuts-and-bolts/routing-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @infinum/ngx-nuts-and-bolts/routing-utils

Secondary entry point of `@infinum/ngx-nuts-and-bolts`. It can be used by importing from `@infinum/ngx-nuts-and-bolts/routing-utils`.
5 changes: 5 additions & 0 deletions libs/ngx-nuts-and-bolts/routing-utils/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
1 change: 1 addition & 0 deletions libs/ngx-nuts-and-bolts/routing-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/route-config-loading';
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
NavigationCancel,
NavigationEnd,
NavigationError,
NavigationStart,
RouteConfigLoadEnd,
RouteConfigLoadStart,
Router,
RouterEvent,
} from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { createRouteConfigLoadingObservable } from './route-config-loading';

class RouterMock {
// eslint-disable-next-line rxjs/no-exposed-subjects, rxjs/finnish
public readonly events = new Subject<RouterEvent>();
}

describe('createRouteConfigLoadingObservable', () => {
let router: RouterMock;
let isRouteConfigLoading$: Observable<boolean>;

beforeEach(() => {
router = new RouterMock();
isRouteConfigLoading$ = createRouteConfigLoadingObservable(router as unknown as Router);
});

it('should emit true/false as RouteConfigLoadStart/RouteConfigLoadEnd events come in', () => {
const callbacks = mockSubscribeCallbacks();
isRouteConfigLoading$.subscribe(callbacks);

expect(callbacks.next).toHaveBeenCalledTimes(0);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new RouteConfigLoadStart({}) as unknown as RouterEvent);

expect(callbacks.next).toHaveBeenCalledTimes(1);
expect(callbacks.next).toHaveBeenNthCalledWith(1, true);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new RouteConfigLoadEnd({}) as unknown as RouterEvent);

expect(callbacks.next).toHaveBeenCalledTimes(2);
expect(callbacks.next).toHaveBeenNthCalledWith(2, false);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);
});

it('should emit false on NavigationError', () => {
const callbacks = mockSubscribeCallbacks();
isRouteConfigLoading$.subscribe(callbacks);

expect(callbacks.next).toHaveBeenCalledTimes(0);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new RouteConfigLoadStart({}) as unknown as RouterEvent);

expect(callbacks.next).toHaveBeenCalledTimes(1);
expect(callbacks.next).toHaveBeenNthCalledWith(1, true);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new NavigationError(0, 'error', {}));

expect(callbacks.next).toHaveBeenCalledTimes(2);
expect(callbacks.next).toHaveBeenNthCalledWith(2, false);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);
});

it('should emit false on NavigationCancel', () => {
const callbacks = mockSubscribeCallbacks();
isRouteConfigLoading$.subscribe(callbacks);

expect(callbacks.next).toHaveBeenCalledTimes(0);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new RouteConfigLoadStart({}) as unknown as RouterEvent);

expect(callbacks.next).toHaveBeenCalledTimes(1);
expect(callbacks.next).toHaveBeenNthCalledWith(1, true);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

router.events.next(new NavigationCancel(0, 'cancel', 'whatever'));

expect(callbacks.next).toHaveBeenCalledTimes(2);
expect(callbacks.next).toHaveBeenNthCalledWith(2, false);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);
});

it('should ignore other events, such as NavigationStart and NavigationEnd', () => {
const callbacks = mockSubscribeCallbacks();
isRouteConfigLoading$.subscribe(callbacks);

expect(callbacks.next).toHaveBeenCalledTimes(0);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);

// TODO: test all other events
router.events.next(new NavigationStart(0, 'foo'));
router.events.next(new NavigationEnd(0, 'foo', 'foo'));

expect(callbacks.next).toHaveBeenCalledTimes(0);
expect(callbacks.error).toHaveBeenCalledTimes(0);
expect(callbacks.complete).toHaveBeenCalledTimes(0);
});
});

function mockSubscribeCallbacks() {
return { next: jest.fn(), complete: jest.fn(), error: jest.fn() };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NavigationCancel, NavigationError, RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router';
import { Observable, filter, map } from 'rxjs';

export function createRouteConfigLoadingObservable(router: Router): Observable<boolean> {
return router.events.pipe(
filter(
(event) =>
event instanceof RouteConfigLoadStart ||
event instanceof RouteConfigLoadEnd ||
event instanceof NavigationError ||
event instanceof NavigationCancel
),
map((event) => event instanceof RouteConfigLoadStart)
);
}
23 changes: 23 additions & 0 deletions ngx-nuts-and-bolts-docs/docs/route-config-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
id: route-config-loading
title: Route config loading
sidebar_label: Route config loading
---

## 1. Features

`createRouteConfigLoadingObservable` function receives `Router` and returns an `Observable` that emits `true`/`false` during route configuration loading. This can be useful to show global loading states during lazy chunk loading. Feel free do add debounce or other operators as needed for your specific use case.

## 2. Usage

```ts
import { createRouteConfigLoadingObservable } from '@infinum/ngx-nuts-and-bolts/routing-utils';

...

class MyComponent {
public readonly isRouteConfigLoading$ = createRouteConfigLoadingObservable(this.router);
}
```

We leave it up to you to decide what to do with this `Observable<boolean>` and hook it up to some loading state indication in your UI.
4 changes: 4 additions & 0 deletions ngx-nuts-and-bolts-docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const sidebars = {
type: 'doc',
id: 'loading-state',
},
{
type: 'doc',
id: 'route-config-loading',
},
{
type: 'doc',
id: 'table-state',
Expand Down
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@infinum/ngx-nuts-and-bolts/form-utils": ["libs/ngx-nuts-and-bolts/form-utils/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/in-view": ["libs/ngx-nuts-and-bolts/in-view/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/loading-state": ["libs/ngx-nuts-and-bolts/loading-state/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/routing-utils": ["libs/ngx-nuts-and-bolts/routing-utils/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/table-state": ["libs/ngx-nuts-and-bolts/table-state/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/testing-utils": ["libs/ngx-nuts-and-bolts/testing-utils/src/index.ts"],
"@infinum/ngx-nuts-and-bolts/utility-types": ["libs/ngx-nuts-and-bolts/utility-types/src/index.ts"],
Expand Down
Loading