WARNING: Please see @MgSam's answer for a fatal bug in my original code. He has a revised version that fixes the bug.
I have made a route interceptor service that has an API to hook into every event that the router broadcasts, and pass in iteratee
, a function that will be invoked on each event and takes in the route as an argument.
Also, the route that is passed in is a modified copy of the original. The difference is that any BehaviorSubject
observables (such as the route data) will be accessible directly as values instead of having the need to subscribe to them.
Here is the implementation of the service:
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouteInterceptorService } from './shared';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(
private routeInterceptorService: RouteInterceptorService,
private titleService: Title
) {}
ngOnInit(): void {
this.routeInterceptorService.getRouteOnNavigationEnd(route => {
this.titleService.setTitle(route.data.title);
});
}
}
As you can see, I'm listening to the NavigationEnd
event and updating the browser title based on the title param in defined on the respective route.
Here is the RouteInterceptorService:
import { Injectable } from '@angular/core';
import {
ActivatedRoute,
Router,
Event,
NavigationStart,
NavigationEnd,
NavigationCancel,
NavigationError,
RoutesRecognized,
RouteConfigLoadStart,
RouteConfigLoadEnd
} from '@angular/router';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/mergeMap';
enum RouteInterceptorEvents {
NAVIGATION_START,
NAVIGATION_END,
NAVIGATION_CANCEL,
NAVIGATION_ERROR,
CONFIG_LOAD_START,
CONFIG_LOAD_END,
ROUTE_RECOGNIZED
}
@Injectable()
export class RouteInterceptorService {
events: Map<number, Observable<Event>> = new Map();
constructor(
private activatedRoute: ActivatedRoute,
private router: Router
) {
this.populateEventMap();
}
getRouteOnNavigationStart(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.NAVIGATION_START, iteratee);
}
getRouteOnNavigationEnd(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.NAVIGATION_END, iteratee);
}
getRouteOnNavigationCancel(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.NAVIGATION_CANCEL, iteratee);
}
getRouteOnNavigationError(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.NAVIGATION_ERROR, iteratee);
}
getRouteOnConfigLoadStart(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.CONFIG_LOAD_START, iteratee);
}
getRouteOnConfigLoadEnd(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.CONFIG_LOAD_END, iteratee);
}
getRouteOnRouteRecognized(iteratee: Function) {
this.getRouteOn(RouteInterceptorEvents.ROUTE_RECOGNIZED, iteratee);
}
private getRouteOn(eventType: number, iteratee: Function) {
this.events[eventType]
// By returning a new Object (this.activatedRoute) into the stream, we
// essentially swap what we're observing.
// At this point we only run .map() if the filtered event (this.events[eventType])
// successfully returns the event, meaning the event of 'eventType' has been triggered.
.map(() => this.activatedRoute)
// Traverse the state tree to find the last activated route, and then
// return it to the stream. Doing this allow sus to dive into the 'children'
// property of the routes config to fetch the corresponding page title(s).
.map(route => {
while (route.firstChild) route = route.firstChild;
return route;
})
// Filter un-named (primary) router outlets only.
.filter(route => route.outlet === 'primary')
// Get the value of all the BehaviorSubject observables in the route so that
// the values can be directly accessed from the route like 'route.data.foo'
// instead of having to subscribe to the observable at the 'iteratee' callback
// level and receive the data that way.
.map(route => {
let keys = Object.keys(route);
let len = keys.length;
for (let i = 0; i < len; i++) {
if (route[keys[i]].getValue) {
route[keys[i]] = route[keys[i]].getValue();
}
}
return route;
})
.subscribe(route => iteratee(route));
}
private populateEventMap() {
this.events[RouteInterceptorEvents.NAVIGATION_START] = this.router.events.filter(e => e instanceof NavigationStart);
this.events[RouteInterceptorEvents.NAVIGATION_END] = this.router.events.filter(e => e instanceof NavigationEnd);
this.events[RouteInterceptorEvents.NAVIGATION_CANCEL] = this.router.events.filter(e => e instanceof NavigationCancel);
this.events[RouteInterceptorEvents.NAVIGATION_ERROR] = this.router.events.filter(e => e instanceof NavigationError);
this.events[RouteInterceptorEvents.CONFIG_LOAD_START] = this.router.events.filter(e => e instanceof RouteConfigLoadStart);
this.events[RouteInterceptorEvents.CONFIG_LOAD_END] = this.router.events.filter(e => e instanceof RouteConfigLoadEnd);
this.events[RouteInterceptorEvents.ROUTE_RECOGNIZED] = this.router.events.filter(e => e instanceof RoutesRecognized);
}
}
Are there any improvements you think that can/should be made to this, or any corner cases that I may not be thinking of?
As is, it works just fine.