Browse Source

Merge pull request #7537 from abpframework/feat/7524

Added a Utility Service for Getting a Stream of Angular Router Events
pull/7552/head
Bunyamin Coskuner 6 years ago
committed by GitHub
parent
commit
33c2767ad4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 146
      docs/en/UI/Angular/Router-Events.md
  2. 4
      docs/en/docs-nav.json
  3. 17
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  4. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  5. 61
      npm/ng-packs/packages/core/src/lib/services/router-events.service.ts
  6. 23
      npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts
  7. 111
      npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts
  8. 17
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts
  9. 22
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

146
docs/en/UI/Angular/Router-Events.md

@ -0,0 +1,146 @@
# Router Events Simplified
`RouterEvents` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose.
## Benefit
You can use router events directly and filter them as seen below:
```js
import {
NavigationEnd,
NavigationError,
NavigationCancel,
Router,
} from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationFinish$ = this.router.events.pipe(
filter(
event =>
event instanceof NavigationEnd ||
event instanceof NavigationError ||
event instanceof NavigationCancel,
),
);
/* Observable<Event> */
constructor(private router: Router) {}
}
```
However, `RouterEvents` makes filtering router events easier.
```js
import { RouterEvents } from '@abp/ng.core';
@Injectable()
class SomeService {
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel');
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */
constructor(private routerEvents: RouterEvents) {}
}
```
`RouterEvents` also delivers improved type-safety. In the example above, `navigationFinish$` has inferred type of `Observable<NavigationCancel | NavigationEnd | NavigationError>` whereas it would have `Observable<Event>` when router events are filtered directly.
## Usage
You do not have to provide `RouterEvents` at the module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components.
### How to Get Specific Navigation Events
You can use `getNavigationEvents` to get a stream of navigation events matching given event keys.
```js
import { RouterEvents } from '@abp/ng.core';
import { merge } from 'rxjs';
import { mapTo } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationStart$ = this.routerEvents.getNavigationEvents('Start');
/* Observable<NavigationStart> */
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel');
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */
loading$ = merge(
this.navigationStart$.pipe(mapTo(true)),
this.navigationFinish$.pipe(mapTo(false)),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get All Navigation Events
You can use `getAllNavigationEvents` to get a stream of all navigation events without passing any keys.
```js
import { RouterEvents, NavigationStart } from '@abp/ng.core';
import { map } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationEvent$ = this.routerEvents.getAllNavigationEvents();
/* Observable<NavigationCancel | NavigationEnd | NavigationError | NavigationStart> */
loading$ = this.navigationEvent$.pipe(
map(event => event instanceof NavigationStart),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get Specific Router Events
You can use `getEvents` to get a stream of router events matching given event constructors.
```js
import { RouterEvents } from '@abp/ng.core';
import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
moduleActivation$ = this.routerEvents.getEvents(ActivationEnd, ChildActivationEnd);
/* Observable<ActivationEnd | ChildActivationEnd> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get All Router Events
You can use `getEvents` to get a stream of all router events without passing any event constructors. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience.
```js
import { RouterEvents } from '@abp/ng.core';
import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
routerEvent$ = this.routerEvents.getAllEvents();
/* Observable<Event> */
constructor(private routerEvents: RouterEvents) {}
}
```

4
docs/en/docs-nav.json

@ -807,6 +807,10 @@
"text": "Easy *ngFor trackBy",
"path": "UI/Angular/Track-By-Service.md"
},
{
"text": "Router Events",
"path": "UI/Angular/Router-Events.md"
},
{
"text": "Inserting Scripts & Styles to DOM",
"path": "UI/Angular/Dom-Insertion-Service.md"

17
npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts

@ -1,11 +1,11 @@
import { Component, Injector, Optional, SkipSelf, Type } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { eLayoutType } from '../enums/common';
import { ABP } from '../models';
import { ReplaceableComponents } from '../models/replaceable-components';
import { LocalizationService } from '../services/localization.service';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { RouterEvents } from '../services/router-events.service';
import { RoutesService } from '../services/routes.service';
import { SubscriptionService } from '../services/subscription.service';
import { findRoute, getRoutePath } from '../utils/route-utils';
@ -44,6 +44,7 @@ export class DynamicLayoutComponent {
private localizationService: LocalizationService,
private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService,
private routerEvents: RouterEvents,
@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent,
) {
if (dynamicLayoutComponent) return;
@ -52,16 +53,16 @@ export class DynamicLayoutComponent {
this.routes = injector.get(RoutesService);
this.getLayout();
this.subscription.addOne(
this.router.events.pipe(filter(event => event instanceof NavigationEnd)),
() => {
this.getLayout();
},
);
this.checkLayoutOnNavigationEnd();
this.listenToLanguageChange();
}
private checkLayoutOnNavigationEnd() {
const navigationEnd$ = this.routerEvents.getNavigationEvents('End');
this.subscription.addOne(navigationEnd$, () => this.getLayout());
}
private getLayout() {
let expectedLayout = (this.route.snapshot.data || {}).layout;

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -15,6 +15,7 @@ export * from './profile.service';
export * from './replaceable-components.service';
export * from './resource-wait.service';
export * from './rest.service';
export * from './router-events.service';
export * from './router-wait.service';
export * from './routes.service';
export * from './session-state.service';

61
npm/ng-packs/packages/core/src/lib/services/router-events.service.ts

@ -0,0 +1,61 @@
import { Injectable, Type } from '@angular/core';
import {
NavigationCancel,
NavigationEnd,
NavigationError,
NavigationStart,
Router,
RouterEvent,
} from '@angular/router';
import { filter } from 'rxjs/operators';
export const NavigationEvent = {
Cancel: NavigationCancel,
End: NavigationEnd,
Error: NavigationError,
Start: NavigationStart,
};
@Injectable({ providedIn: 'root' })
export class RouterEvents {
constructor(private router: Router) {}
getEvents<T extends RouterEventConstructors>(...eventTypes: T) {
type FilteredRouterEvent = T extends Type<infer Ctor>[] ? Ctor : never;
const filterRouterEvents = (event: RouterEvent): event is FilteredRouterEvent =>
eventTypes.some(type => event instanceof type);
return this.router.events.pipe(filter(filterRouterEvents));
}
getNavigationEvents<T extends NavigationEventKeys>(...navigationEventKeys: T) {
type FilteredNavigationEvent = T extends (infer Key)[]
? Key extends NavigationEventKey
? InstanceType<NavigationEventType[Key]>
: never
: never;
const filterNavigationEvents = (event: RouterEvent): event is FilteredNavigationEvent =>
navigationEventKeys.some(key => event instanceof NavigationEvent[key]);
return this.router.events.pipe(filter(filterNavigationEvents));
}
getAllEvents() {
return this.router.events;
}
getAllNavigationEvents() {
const keys = Object.keys(NavigationEvent) as NavigationEventKeys;
return this.getNavigationEvents(...keys);
}
}
type RouterEventConstructors = [Type<RouterEvent>, ...Type<RouterEvent>[]];
type NavigationEventKeys = [NavigationEventKey, ...NavigationEventKey[]];
type NavigationEventType = typeof NavigationEvent;
export type NavigationEventKey = keyof NavigationEventType;

23
npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts

@ -1,9 +1,10 @@
import { Injectable, Injector } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';
import { filter, map, mapTo, switchMap, takeUntil, tap } from 'rxjs/operators';
import { InternalStore } from '../utils/internal-store-utils';
import { NavigationStart } from '@angular/router';
import { of, Subject, timer } from 'rxjs';
import { map, mapTo, switchMap, takeUntil, tap } from 'rxjs/operators';
import { LOADER_DELAY } from '../tokens/lodaer-delay.token';
import { InternalStore } from '../utils/internal-store-utils';
import { RouterEvents } from './router-events.service';
export interface RouterWaitState {
loading: boolean;
@ -16,17 +17,15 @@ export class RouterWaitService {
private store = new InternalStore<RouterWaitState>({ loading: false });
private destroy$ = new Subject();
private delay: number;
constructor(private router: Router, injector: Injector) {
constructor(private routerEvents: RouterEvents, injector: Injector) {
this.delay = injector.get(LOADER_DELAY, 500);
this.router.events
this.updateLoadingStatusOnNavigationEvents();
}
private updateLoadingStatusOnNavigationEvents() {
this.routerEvents
.getAllNavigationEvents()
.pipe(
filter(
event =>
event instanceof NavigationStart ||
event instanceof NavigationEnd ||
event instanceof NavigationError ||
event instanceof NavigationCancel,
),
map(event => event instanceof NavigationStart),
switchMap(condition =>
condition

111
npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts

@ -0,0 +1,111 @@
import {
NavigationCancel,
NavigationEnd,
NavigationError,
NavigationStart,
ResolveEnd,
ResolveStart,
Router,
RouterEvent,
} from '@angular/router';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { NavigationEventKey, RouterEvents } from '../services/router-events.service';
describe('RouterEvents', () => {
let spectator: SpectatorService<RouterEvents>;
let service: RouterEvents;
const events = new Subject<RouterEvent>();
const emitRouterEvents = () => {
events.next(new RouterEvent(0, null));
events.next(new NavigationStart(1, null, null));
events.next(new ResolveStart(2, null, null, null));
events.next(new RouterEvent(3, null));
events.next(new NavigationError(4, null, null));
events.next(new NavigationEnd(5, null, null));
events.next(new ResolveEnd(6, null, null, null));
events.next(new NavigationCancel(7, null, null));
};
const createService = createServiceFactory({
service: RouterEvents,
providers: [
{
provide: Router,
useValue: { events },
},
],
});
beforeEach(() => {
spectator = createService();
service = spectator.service;
});
describe('getNavigationEvents', () => {
test.each`
filtered | expected
${['Start', 'Cancel']} | ${[1, 7]}
${['Error', 'Cancel']} | ${[4, 7]}
${['Start', 'End']} | ${[1, 5]}
${['Error', 'End']} | ${[4, 5]}
`(
'should return a stream of given navigation events',
({ filtered, expected }: NavigationEventTest) => {
const stream = service.getNavigationEvents(...filtered);
const collected: number[] = [];
stream.pipe(take(2)).subscribe(event => collected.push(event.id));
emitRouterEvents();
expect(collected).toEqual(expected);
},
);
});
describe('getAnyNavigationEvent', () => {
it('should return a stream of any navigation event', () => {
const stream = service.getAllNavigationEvents();
const collected: number[] = [];
stream.pipe(take(4)).subscribe(event => collected.push(event.id));
emitRouterEvents();
expect(collected).toEqual([1, 4, 5, 7]);
});
});
describe('getEvents', () => {
it('should return a stream of given router events', () => {
const stream = service.getEvents(ResolveEnd, ResolveStart);
const collected: number[] = [];
stream.pipe(take(2)).subscribe(event => collected.push(event.id));
emitRouterEvents();
expect(collected).toEqual([2, 6]);
});
});
describe('getAnyEvent', () => {
it('should return a stream of any router event', () => {
const stream = service.getAllEvents();
const collected: number[] = [];
stream.pipe(take(8)).subscribe((event: RouterEvent) => collected.push(event.id));
emitRouterEvents();
expect(collected).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
});
});
});
interface NavigationEventTest {
filtered: [NavigationEventKey, ...NavigationEventKey[]];
expected: number[];
}

17
npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts

@ -1,7 +1,14 @@
import { ABP, getRoutePath, RoutesService, TreeNode, SubscriptionService } from '@abp/ng.core';
import {
ABP,
getRoutePath,
RouterEvents,
RoutesService,
SubscriptionService,
TreeNode,
} from '@abp/ng.core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs/operators';
import { Router } from '@angular/router';
import { map, startWith } from 'rxjs/operators';
import { eThemeSharedRouteNames } from '../../enums';
@Component({
@ -18,12 +25,12 @@ export class BreadcrumbComponent implements OnInit {
private router: Router,
private routes: RoutesService,
private subscription: SubscriptionService,
private routerEvents: RouterEvents,
) {}
ngOnInit(): void {
this.subscription.addOne(
this.router.events.pipe(
filter<NavigationEnd>(event => event instanceof NavigationEnd),
this.routerEvents.getNavigationEvents('End').pipe(
// tslint:disable-next-line:deprecation
startWith(null),
map(() => this.routes.search({ path: getRoutePath(this.router) })),

22
npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

@ -1,4 +1,4 @@
import { AuthService, LocalizationParam, RestOccurError } from '@abp/ng.core';
import { AuthService, LocalizationParam, RestOccurError, RouterEvents } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http';
import {
ApplicationRef,
@ -10,7 +10,7 @@ import {
Injector,
RendererFactory2,
} from '@angular/core';
import { NavigationError, ResolveEnd, Router } from '@angular/router';
import { NavigationError, ResolveEnd } from '@angular/router';
import { Actions, ofActionSuccessful } from '@ngxs/store';
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@ -72,7 +72,7 @@ export class ErrorHandler {
constructor(
private actions: Actions,
private router: Router,
private routerEvents: RouterEvents,
private confirmationService: ConfirmationService,
private cfRes: ComponentFactoryResolver,
private rendererFactory: RendererFactory2,
@ -85,20 +85,16 @@ export class ErrorHandler {
}
private listenToRouterError() {
this.router.events
.pipe(
filter(event => event instanceof NavigationError),
filter(this.filterRouteErrors),
)
this.routerEvents
.getNavigationEvents('Error')
.pipe(filter(this.filterRouteErrors))
.subscribe(() => this.show404Page());
}
private listenToRouterDataResolved() {
this.router.events
.pipe(
filter(event => event instanceof ResolveEnd),
filter(() => !!this.componentRef),
)
this.routerEvents
.getEvents(ResolveEnd)
.pipe(filter(() => !!this.componentRef))
.subscribe(() => {
this.componentRef.destroy();
this.componentRef = null;

Loading…
Cancel
Save