mirror of https://github.com/abpframework/abp.git
committed by
GitHub
9 changed files with 364 additions and 38 deletions
@ -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) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
@ -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; |
||||
@ -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[]; |
||||
|
} |
||||
Loading…
Reference in new issue