Skip to content

Commit

Permalink
Merge pull request #7537 from abpframework/feat/7524
Browse files Browse the repository at this point in the history
Added a Utility Service for Getting a Stream of Angular Router Events
  • Loading branch information
bnymncoskuner authored Jan 31, 2021
2 parents 401c652 + 36eb7f9 commit 33c2767
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 38 deletions.
146 changes: 146 additions & 0 deletions docs/en/UI/Angular/Router-Events.md
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions docs/en/docs-nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions npm/ng-packs/packages/core/src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 12 deletions npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Loading

0 comments on commit 33c2767

Please sign in to comment.