Sean

Angular Tips: Global access to route params

Author: Sean Wright on 02/03/2017

The new Angular Router is powerful, relying on observables, pattern matching, conventions and customizations (Resolves or Guards) but it is an evolving project that doesn't yet cater to every use case.

If your Angular application uses the router then your components have access to all the routing information that led to them being initialized like the url, tokenized parameters, query string values and url fragments. This information is made available through the Router, ActivatedRoute and ActivatedRouteSnapshot services.

For example if you have a route defined as 'users/:user_id/accounts' and the url you navigate to in your app is /users/53/accounts then you can access the id of the user by injecting the ActivatedRouteSnapshot service into your component and accessing activatedRouteSnapshot.params['user_id'].

Unfortunately as soon as you start using child routes you lose access to any parameters defined in the part of the route that loads the parent component and likewise for parent components not being able to access child route parameters.

Let's look at this example route definition

const routes: Routes = [
    {
        path: 'global-users/access/:user_id',
        component: UserAccessComponent,
		canActivate: [AuthGuard],
    },
    {
        path: 'users/:user_id/accounts',
        component: UserRootComponent,
        canActivateChild: [AuthGuard],
        resolve: [InvitationServiceResolve],
        children: [
			{ path: ':account_id', component: AccountDetailComponent },
            { path: '', component: AccountListComponent }
        ]
    }
];

If you have a route structure like this you've possibly run into the problem I described above. The UserRootComponent has access to user_id but not account_id and the AccountDetailComponent has access to the account_id but not the user_id.

My solution has been to use a global service and an observable pipeline in my root AppComponent.

Below is the RouteHelper service which exposes an observable params that any component can have access to by injecting the RouteHelper. The params observable is a stream of objects with keys matching the route tokens and values matching those live url values.

// './shared/route-helper.service'
import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class RouterHelper {
    params: Observable;

    private paramsSource: BehaviorSubject;

    constructor() {
        this.paramsSource = new BehaviorSubject({});
        this.params = this.paramsSource.asObservable();
    }

    setParams(params: Params) {
        this.paramsSource.next(params);
    }
}

And below is the observable pipeline which delivers global route data to the RouteHelper service.

First we watch all router events, filtering for only completed navitations. These are then mapped to the ActivatedRoute which is used to build up an array of observables of the Param objects of each route segment going from the root to the furthest child. Then using combineLatest and mergeMap we go from an array of observables of Params to an array of Params which we can reduce to an object with keys of each parameter token and values of those token's values. This object is then sent to the RouteHelper service.

// './AppComponent.ts'
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/combineLatest';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

import { RouterHelper } from './shared/shared.module';

@Component({
    selector: 'body',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {

    constructor(
        private route: ActivatedRoute,
        private router: Router,
        private routerHelper: RouterHelper
    ) { }

    ngOnInit() {
        this.router.events
            .filter(event => event instanceof NavigationEnd)
            .map(() => this.route)
            .map(route => {
                let params: Array> = [];
                params.push(route.params);

                while (route.firstChild) {
                    route = route.firstChild;
                    params.push(route.params);
                }

                return Observable.combineLatest(params);
            })
            .mergeMap(params => params)
            .map(params => params.reduce((prev, curr) => {
                Object.keys(curr).forEach(key => {
                    prev[key] = curr[key];
                });

                return prev;
            }, {}))
            .subscribe(p => this.routerHelper.setParams(p));
    }
}

It should be noted this approach doesn't handle nested child or auxiliary routes but if this is your use-case you cuold create a more complex pipeline to grab all those params or directly query the sibling, child or parent route parameters.

To use this data, inject the RouteHelper service into your desired component, subscribe to the params observable and then access the parameter you want using its key.

this.routeHelper.params
    .subscribe(params => {
        console.log(`user_id ${params['user_id']}, account_id ${params['account_id']}`);
    });