Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandy Clark
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandy Clark@sandraclarktw )

Accessing Parent And Child Route Segment Parameters In Angular 4.4.4

By Ben Nadel on

The other day, I took a look at the Angular 4 Router; and, having not looked at it since the RC (Release Candidate) days, I was very happy to see that conditional router-outlets mostly work in the latest version of Angular. In fact, the Angular 4 Router seem, in general, much easier to use than it used to be. One of the features that has been greatly simplified is activated route traversal. Now, if you want to access a Parent or Child route's parameters, you can easily walk up and down the ActivatedRoute hierarchy.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To demonstrate the ease with which you can access a parent or child route segment's parameters, I'm going to bring back the demo from the other week; but, I'm going to update it such that each View component will read the other View component's route params. Since this is a Parent / Child relationship demo, the Parent component will access the Child component's :id; and, the Child component will access the Parent component's :id.

As a reminder, the routing for this demo uses a single parent/child relationship that is rendered within the App component. You can see the route configuration in my app module:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  • import { Routes } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { ChildComponent } from "./child.component";
  • import { ParentComponent } from "./parent.component";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • var routes: Routes = [
  • {
  • path: "parent/:id",
  • component: ParentComponent,
  • children: [
  • {
  • path: "child/:id",
  • component: ChildComponent
  • }
  • ]
  • },
  • {
  • path: "**",
  • redirectTo: "/"
  • }
  • ];
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • bootstrap: [
  • AppComponent
  • ],
  • imports: [
  • BrowserModule,
  • RouterModule.forRoot(
  • routes,
  • {
  • // Tell the router to use the HashLocationStrategy.
  • useHash: true
  • }
  • )
  • ],
  • declarations: [
  • AppComponent,
  • ChildComponent,
  • ParentComponent
  • ],
  • providers: [
  • // CAUTION: We don't need to specify the LocationStrategy because we are setting
  • // the "useHash" property in the Router module above.
  • // --
  • // {
  • // provide: LocationStrategy,
  • // useClass: HashLocationStrategy
  • // }
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

Notice that in each of the route segment definitions, there is a single route parameter named :id. Since route parameters are isolated within their own segments, there is no problem with having the same name for unrelated parameters - they will not overwrite each other. That said, you may choose to use unique names so as to make the code a bit more readable (for example, using :parentID and :childID).

The App component doesn't really play much of a role in this demo other than presenting the navigation and providing the first router-outlet (for the parent component). But, just to add clarity to the overall architecture, I'll share it again (unchanged from the previous demo):

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { Event as NavigationEvent } from "@angular/router";
  • import { NavigationEnd } from "@angular/router";
  • import { Router } from "@angular/router";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.css" ],
  • template:
  • `
  • <h2>
  • App Routing Component
  • </h2>
  •  
  • <ul>
  • <li>
  • <a routerLink="/">Home</a>
  • <strong *ngIf="activated.home">&laquo;&mdash;Selected</strong>
  • </li>
  • <li>
  • <a routerLink="/parent/p1/child/p1-c1">Route: /parent/p1/child/p1-c1</a>
  • <strong *ngIf="activated.p1c1">&laquo;&mdash;Selected</strong>
  • </li>
  • <li>
  • <a routerLink="/parent/p1/child/p1-c2">Route: /parent/p1/child/p1-c2</a>
  • <strong *ngIf="activated.p1c2">&laquo;&mdash;Selected</strong>
  • </li>
  • <li>
  • <a routerLink="/parent/p2/child/p2-c1">Route: /parent/p2/child/p2-c1</a>
  • <strong *ngIf="activated.p2c1">&laquo;&mdash;Selected</strong>
  • </li>
  • <li>
  • <a routerLink="/parent/p2/child/p2-c2">Route: /parent/p2/child/p2-c2</a>
  • <strong *ngIf="activated.p2c2">&laquo;&mdash;Selected</strong>
  • </li>
  • </ul>
  •  
  • <router-outlet></router-outlet>
  • `
  • })
  • export class AppComponent {
  •  
  • public activated: {
  • home: boolean;
  • p1c1: boolean;
  • p1c2: boolean;
  • p2c1: boolean;
  • p2c2: boolean;
  • };
  •  
  • private router: Router;
  •  
  • // I initialize the app component.
  • constructor( router: Router ) {
  •  
  • this.router = router;
  • this.activated = {
  • home: false,
  • p1c1: false,
  • p1c2: false,
  • p2c1: false,
  • p2c2: false
  • };
  •  
  • // Listen for routing events so we can update the activated route indicator
  • // as the user navigates around the application.
  • this.router.events.subscribe(
  • ( event: NavigationEvent ) : void => {
  •  
  • if ( event instanceof NavigationEnd ) {
  •  
  • this.activated.home = this.router.isActive( "/", true );
  • this.activated.p1c1 = this.router.isActive( "/parent/p1/child/p1-c1", true );
  • this.activated.p1c2 = this.router.isActive( "/parent/p1/child/p1-c2", true );
  • this.activated.p2c1 = this.router.isActive( "/parent/p2/child/p2-c1", true );
  • this.activated.p2c2 = this.router.isActive( "/parent/p2/child/p2-c2", true );
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  • }

Ok, now we can get into the real Parent / Child View component relationship. In Angular, each View component is associated with an ActivatedRoute instance. But, unlike many other services in Angular, ActivatedRoute is not a singleton. Instead, each View component is injected with its own unique instance of ActivatedRoute.

This ActivatedRoute instance gives the View component access to the state of the local route segment. This means that in the ParentComponent, the injected ActivatedRoute instance holds state information about the segment, "parent/:id"; and, in the ChildComponent, the injected ActivatedRoute instance holds state information about the segment, "child/:id".

These ActivateRoute instance, although injected into different View components, are connected through segment traversal properties. The "parent" property allows one ActivatedRoute to access the ActivateRoute of its parent segment. And, the "firstChild" property allows one ActivatedRoute to access the ActivatedRoute of its child segment.

NOTE: The ActivatedRoute has both a "firstChild" and a "children" property. At this point, the full scope of the "children" property is beyond my understanding. I assume it has to do with having multiple, named router-outlet components; but, that is not something I have experience with.

Now that we understand how to traverse from one ActivatedRoute instance to another, let's look at our Parent / Child relationship. In the ParentComponent, whenever the route changes, we're going to render both the "id" of the parent segment and the "childID" of the child segment. Since both of these may change while the ParentComponent is rendered, we'll have to subscribe to both sets of paramMap Observables:

  • // Import the core angular services.
  • import { ActivatedRoute } from "@angular/router";
  • import { Component } from "@angular/core";
  • import { ParamMap } from "@angular/router";
  • import { Subscription } from "rxjs/Subscription";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-parent",
  • styleUrls: [ "./parent.component.css" ],
  • template:
  • `
  • <h3>
  • Parent Routing Component
  • </h3>
  •  
  • <ng-template [ngIf]="isLoading">
  •  
  • <p>
  • <em>Loading parent data...</em>
  • </p>
  •  
  • </ng-template>
  •  
  • <ng-template [ngIf]="! isLoading">
  •  
  • <div class="view">
  • <p>
  • Parent data is <strong>loaded</strong>
  • </p>
  •  
  • <p>
  • <strong>Parent ID</strong>: {{ id }}<br />
  • <strong>Child ID</strong>: {{ childID }}
  • </p>
  •  
  • <router-outlet></router-outlet>
  • </div>
  •  
  • </ng-template>
  • `
  • })
  • export class ParentComponent {
  •  
  • public childID: string;
  • public id: string;
  • public isLoading: boolean;
  •  
  • private childParamSubscription: Subscription;
  • private paramSubscription: Subscription;
  • private timer: number;
  •  
  • // I initialize the parent component.
  • constructor( activatedRoute: ActivatedRoute ) {
  •  
  • console.warn( "Parent component initialized." );
  •  
  • this.childID = "";
  • this.id = "";
  • this.isLoading = true;
  • this.timer = null;
  •  
  • // Get the current route segment's :id. While the Parent Component is rendered,
  • // it's possible that the :id parameter in the route will change. As such, we
  • // want to subscribe to the activated route so that we can load the new data as
  • // the :id value changes (this will also give us access to the FIRST id value
  • // as well).
  • // --
  • // NOTE: If you only wanted the initial value of the parameter, you could use the
  • // route snapshot - activatedRoute.snapshot.paramMap.get( "id" ).
  • this.paramSubscription = activatedRoute.paramMap.subscribe(
  • ( params: ParamMap ) : void => {
  •  
  • console.log( "Parent ID changed:", params.get( "id" ) );
  •  
  • this.id = params.get( "id" );
  •  
  • // Simulate loading the data from some external service.
  • this.isLoading = true;
  • this.timer = this.timer = setTimeout(
  • () : void => {
  •  
  • this.isLoading = false;
  •  
  • },
  • 1000
  • );
  •  
  • }
  • );
  •  
  • // Get the child route segment's :id. The ActivatedRoute provides a simple means
  • // to walk up and down the route hierarchy. The "firstChild" property gives us
  • // direct access to the ActivatedRoute instance associated with the child segment
  • // of the current route. Since the child segment's :id can change while the
  • // Parent Component is rendered, we want to subscribe to the changes.
  • this.childParamSubscription = activatedRoute.firstChild.paramMap.subscribe(
  • ( params: ParamMap ) : void => {
  •  
  • this.childID = params.get( "id" );
  •  
  • }
  • );
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I get called once when the component is being unmounted.
  • public ngOnDestroy() : void {
  •  
  • console.warn( "Parent component destroyed." );
  •  
  • // When the Parent component is destroyed, we need to stop listening for param
  • // changes so that we don't continually load data in the background.
  • // --
  • // CAUTION: The Angular documentation indicates that you don't need to do this
  • // for ActivatedRoute observables; however, if you log the changes, you will
  • // see that the observables don't always get torn-down when the route component
  • // is destroyed. As such, it should be considered a best practice to always
  • // unsubscirbe from observables.
  • this.paramSubscription.unsubscribe();
  • this.childParamSubscription.unsubscribe();
  • clearTimeout( this.timer );
  •  
  • }
  •  
  • }

As you can see, we're using the "firstChild" property of the injected ActivatedRoute to access the ActivatedRoute associated with the child segment. Then, we're subscribing to both paramMap instances:

  • activatedRoute.paramMap.subscribe( ... )
  • activatedRoute.firstChild.paramMap.subscribe( ... )

Conversely, in the ChildComponent, we'll be using the "parent" property to access the ActivatedRoute associated with the parent segment. However, since we know that in this application, the ChildComponent will be destroyed whenever the parent segment's :id changes, we don't have to subscribe to the parent's paramMap (like we did above) - we can just look at the snapshot:

  • // Import the core angular services.
  • import { ActivatedRoute } from "@angular/router";
  • import { Component } from "@angular/core";
  • import { ParamMap } from "@angular/router";
  • import { Subscription } from "rxjs/Subscription";
  •  
  • // Import these modules for their side-effects.
  • import "rxjs/add/operator/delay";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-child",
  • styleUrls: [ "./child.component.css" ],
  • template:
  • `
  • <h4>
  • Child Routing Component
  • </h4>
  •  
  • <ng-template [ngIf]="isLoading">
  •  
  • <p>
  • <em>Loading child data...</em>
  • </p>
  •  
  • </ng-template>
  •  
  • <ng-template [ngIf]="! isLoading">
  •  
  • <div class="view">
  • <p>
  • Child data is <strong>loaded</strong>.
  • </p>
  •  
  • <p>
  • <strong>Parent ID</strong>: {{ parentID }}<br />
  • <strong>Child ID</strong>: {{ id }}
  • </p>
  • </div>
  •  
  • </ng-template>
  • `
  • })
  • export class ChildComponent {
  •  
  • public id: string;
  • public isLoading: boolean;
  • public parentID: string;
  •  
  • private paramSubscription: Subscription;
  • private timer: number;
  •  
  • // I initialize the child component.
  • constructor( activatedRoute: ActivatedRoute ) {
  •  
  • console.warn( "Child component initialized." );
  •  
  • this.id = "";
  • this.isLoading = true;
  • this.parentID = "";
  • this.timer = null;
  •  
  • // Get the parent route segment's :id parameter. The ActivatedRoute provides
  • // a simple means to walk up the route hierarchy. The "parent" property gives
  • // you direct access to the ActivatedRoute instance associated with the parent
  • // segment of the current route. We could "subscribe" to the parent route
  • // segment, the same way we subscribe to the current route segment (below); but,
  • // since we know that the Child Component will be destroyed when the parent
  • // segment changes, we can simply use the snapshot of the parent.
  • this.parentID = activatedRoute.parent.snapshot.paramMap.get( "id" );
  •  
  • // Get the current route segment's :id parameter. While the Child Component is
  • // rendered, it's possible that the :id parameter in the route will change. As
  • // such, we want to subscribe to the activated route so that we can load the new
  • // data as the :id value changes (this will also give us access to the FIRST id
  • // value as well).
  • // --
  • // NOTE: If you only wanted the initial value of the parameter, you could use the
  • // route snapshot - activatedRoute.snapshot.paramMap.get( "id" ).
  • this.paramSubscription = activatedRoute.paramMap
  • // TIMING HACK: We need a tick-delay to allow ngOnDestroy() to fire first
  • // (before our subscribe function is invoked) if the route changes in such a
  • // way that it has to destroy the Child component before it re-renders it
  • // (such as navigating from "p1-c1" to "p2-c1).
  • .delay( 0 )
  • .subscribe(
  • ( params: ParamMap ) : void => {
  •  
  • console.log( "Child ID changed:", params.get( "id" ) );
  •  
  • this.id = params.get( "id" );
  •  
  • // Simulate loading the data from some external service.
  • this.isLoading = true;
  • this.timer = setTimeout(
  • () : void => {
  •  
  • this.isLoading = false;
  •  
  • },
  • 1000
  • );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I get called once when the component is being unmounted.
  • public ngOnDestroy() : void {
  •  
  • console.warn( "Child component destroyed." );
  •  
  • // When the Child component is destroyed, we need to stop listening for param
  • // changes so that we don't continually load data in the background.
  • // --
  • // CAUTION: The Angular documentation indicates that you don't need to do this
  • // for ActivatedRoute observables; however, that seems to ONLY BE TRUE if you
  • // navigate to a DIFFERENT ROUTE PATTERN. If you remain in the ROUTE PATTERN,
  • // but do so in a way that this component is destroyed, the Router WILL NOT
  • // automatically unsubscribe from the Observable (ex, going from "p1-c1" to
  • // "p2-c2"). As such, it is a best practice to ALWAYS unsubscribe from changes,
  • // regardless of what the documentation says.
  • this.paramSubscription.unsubscribe();
  • clearTimeout( this.timer );
  •  
  • }
  •  
  • }

As you can see, in this View, we're using the "parent" property of the injected ActivatedRoute to access the parent's snapshot:

  • activatedRoute.parent.snapshot.paramMap.get( "id" )
  • activatedRoute.paramMap.delay( 0 ).subscribe( ... )

Now, if we run this application in the browser and access one of the routes, we can see that the ParentComponent can access the ChildComponent's route segment; and, the ChildComponent can access the ParentComponent's route segment:


 
 
 

 
 Accessing the paramMap from a different View component using the ActivatedRoute instance. 
 
 
 

As you can see, each View component was able to access the paramMap from the other View component by traversing the ActivatedRoute hierarchy. This is so much easier than it used to be (at least from what I can remember back in the Release Candidate days). It's exciting to see the Angular Router really taking shape. I think it's about time that I revisit my earlier ngRx Router Exploration to see if I can rebuild it with the latest Angular 4 Router.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Thanks Ben.
Is there any global method which can get any parameters in the location? Like this: let id = GlobalRoute.get("id");
If not, do you know why?

Reply to this Comment

@Yu,

There is a way to grab parameters from anywhere; but, not in the way that you are thinking. The Router service provides a "RouterState" object, which exposes a Tree of ActivatedRoute objects. You can then traverse that tree and look for parameters with given names. But, the parameters aren't naturally available at a top-level.

The problem is that there's not constraint in the Router that says that parameters names have to be uniquely named (across multiple Activated routes). So, you could have a URL that looks like this:

foo/:id/bar/:id/baz/:id

... and as long as each :id parameter belonged to a different ActivatedRoute segment, then there would be no collision of names:

( foo / :id ) / ( bar / :id ) / ( baz / :id )

So, even if you were to traverse the Tree of ActivatedRoutes, you'd still find multiple parameters all with the name, "id".

That said, it would be a fun demonstrate -- I'll put something together and get it posted.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.