HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptMinor

Angular2 Route Interceptor

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
routeangular2interceptor

Problem

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_

Solution

I found this post because I needed something that did what you implemented. I tried to use the code and ran into some nasty routing bugs whereby Angular was experiencing internal errors. The cause is this bit:

// 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;
        })


This is extremely dangerous. You are modifying an object that you do not own, an object which Angular relies upon to do its routing. Angular can change how they use that object inside the routing module at any time. Changing the properties here is asking for trouble.

In general, you should never modify an object that you didn't create yourself, regardless of which framework you're using.

As an aside, I think the comments are great and, in addition, you should add JSDoc comments to the public surface area of this class.

In case anyone in the future needs something they can just copy and paste- here is a version without the offending code:

```
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';

/**
* This service intercepts all routing requests going through Angular.
* Source: https://codereview.stackexchange.com/questions/161783/angular2-route-interceptor
*/
@Injectable()
export class RouteInterceptor {
private events: Map> = new Map();

constructor(
private activatedRoute: ActivatedRoute,
private router: Router
) {
this.populateEventMap();
}

/**
* Signs up a callback for when navigation starts.
* @param callback The callback function to invoke.
*/
onNavigationStart(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.NavigationStart, callback);
}

/**
* Signs up a callback for when navigation ends.
* @param callback The callback function to invoke.
*/
onNavigationEnd(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.NavigationEnd, callback);
}

/**
* Signs up a callback for when navigation is cancelled.
* @param callback The callback function to invoke.
*/
onNavigationCancel(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.NavigationCancel, callback);
}

/**
* Signs up a callback for when navigation errors.
* @param callback The callback function to invoke.
*/
onNavigationError(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.NavigationError, callback);
}

/**
* Signs up a callback for when configuration load starts.
* @param callback The callback function to invoke.
*/
onConfigLoadStart(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.ConfigLoadStart, callback);
}

/**
* Signs up a callback for when configuration load ends.
* @param callback The callback function to invoke.
*/
onConfigLoadEnd(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.ConfigLoadEnd, callback);
}

/**
* Signs up a callback for when the route is recongized.
* @param callback The callback function to invoke.
*/
onRouteRecognized(callback: (router: ActivatedRoute) => void) {
this.getRouteOn(RouteInterceptorEvents.RouteRecognized, callback);
}

private getRouteOn(eventType: number, callback: (router: ActivatedRoute) => void) {
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 allows us to dive into the 'children'
// property of the routes config to fetch the cor

Code Snippets

// 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;
        })
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';


/**
 * This service intercepts all routing requests going through Angular.
 * Source: https://codereview.stackexchange.com/questions/161783/angular2-route-interceptor
 */
@Injectable()
export class RouteInterceptor {
    private events: Map<number, Observable<Event>> = new Map();

    constructor(
        private activatedRoute: ActivatedRoute,
        private router: Router
    ) {
        this.populateEventMap();
    }

    /**
     * Signs up a callback for when navigation starts.
     * @param callback The callback function to invoke.
     */
    onNavigationStart(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.NavigationStart, callback);
    }

    /**
     * Signs up a callback for when navigation ends.
     * @param callback The callback function to invoke.
     */
    onNavigationEnd(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.NavigationEnd, callback);
    }

    /**
     * Signs up a callback for when navigation is cancelled.
     * @param callback The callback function to invoke.
     */
    onNavigationCancel(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.NavigationCancel, callback);
    }

    /**
     * Signs up a callback for when navigation errors.
     * @param callback The callback function to invoke.
     */
    onNavigationError(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.NavigationError, callback);
    }

    /**
     * Signs up a callback for when configuration load starts.
     * @param callback The callback function to invoke.
     */
    onConfigLoadStart(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.ConfigLoadStart, callback);
    }

    /**
     * Signs up a callback for when configuration load ends.
     * @param callback The callback function to invoke.
     */
    onConfigLoadEnd(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.ConfigLoadEnd, callback);
    }

    /**
     * Signs up a callback for when the route is recongized.
     * @param callback The callback function to invoke.
     */
    onRouteRecognized(callback: (router: ActivatedRoute) => void) {
        this.getRouteOn(RouteInterceptorEvents.RouteRecognized, callback);
    }

    private getRouteOn(eventType: number, callback: (router: ActivatedRoute) => void) {
        this.events[eventType]
            // By returning a new Object (this.activatedRoute) into the stream,

Context

StackExchange Code Review Q#161783, answer score: 4

Revisions (0)

No revisions yet.