Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Mike Frey
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Mike Frey

Accessing Parent And Child Route Segment Parameters In Angular 4.4.4

By
Published in Comments (11)

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.

Want to use code from this post? Check out the license.

Reader Comments

2 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?

15,841 Comments

@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.

1 Comments

hi

how i can read queryParams to child component from parent component

if my app is sidemenu application, from list i get one item and will get new view simultaneously sidebar category will be change as per the selected item

when i change sidebar category i have to get the id in my child component, and url also need to show id, is it possible??
pls advice

15,841 Comments

@Rajesh,

I don't believe that "queryParams" have a sense of hierarchy. From what I understand, the philosophy of the Angular Router is that the query-params are a global construct. And, they persist across navigational actions. It's only the segment-params that are specific to any activate-route within the segment hierarchy. As such, those are the only aspects (path-params) that have a parent-child relationship.

If you want to access the query-params, I think you just have to inject the ActivatedRoute and access the .queryParams property. This property is shared by all ActivatedRoute instances.

If you are not in the middle of a Component that can inject an ActivatedRoute, you should be able to inject the Router and access the .routeState and then get the .root property, which is the top of the ActivatedRoute hierarchy.

4 Comments

Hope I can explain this well enough:

I have a sub module that has a set of routes that have data: { showDialog: false; } on some of the routes. For that router-outlet, how can I grab that data when the route changes so in the parent I can determine whether to show the dialog? I have tried snapshots but it does not pick up the data in the parent.

For example,

<app-dialog *ngIf="showDialog"></app-dialog>
<router-outlet></router-outlet>

I need to be able to have the parent component that has the app-dialog component pick up the data from the route so that it knows if the dialog should show or not? Does this make sense?

15,841 Comments

@Rob,

I am not sure I fully understand the use-case, but, it sounds like you are just having problems accessing the data attribute of the ActivatedRouteSnapshot instance. It sounds like you might just need to go up to the parent via the activatedRoute.parent property. Then, you should be able to access .snapshot.data off of the parent ActivatedRoute instance.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel