Angular 2's component router offers you the necessary concept of child routes. As you might expect, this brings the concept of recursively defined views to the table, which affords you an incredibly useful and elegant way of building your application.
The code, links, and a live example of this are available at .
Begin with the Array and anchor-tag-based implementation shown in Navigating with routerLinks recipe.
Your goal is to extend this simple application to include /article
, which will be the list view, and /article/:id
, which will be the detail view.
First, modify the route structure for this simple application by extending the /article
path to include its subpaths: /
and /:id
. Routes are defined hierarchically, and each route can have child routes using the children property.
First, you must modify the existing ArticleComponent
so that it can contain child views. As you might expect, the child view is rendered in exactly the same way as it is done from the root component, using RouterOutlet
:
[app/article.component.ts] import {Component} from '@angular/core'; @Component({ template: ` <h2>Article</h2> <router-outlet></router-outlet> ` }) export class ArticleComponent {}
This won't do anything yet, but adding RouterOutlet
describes to Angular how route component hierarchies should be rendered.
In this recipe, you would like to have the parent ArticleComponent
contain a child view, either ArticleListComponent
or ArticleDetailComponent
. For the simplicity of this recipe, you can just define your list of articles as an array of integers.
Define the skeleton of these two components as follows:
[app/article-list.component.ts] import {Component} from '@angular/core'; @Component({ template: ` <h3>Article List</h3> ` }) export class ArticleListComponent { articleIds:Array<number> = [1,2,3,4,5]; } [app/article-detail.component.ts] import {Component} from '@angular/core'; @Component({ template: ` <h3>Article Detail</h3> <p>Showing article {{articleId}}</p> ` }) export class ArticleDetailComponent { articleId:number; }
At this point, nothing in the application yet points to either of these child routes, so you'll need to define them now.
The children
property of a route should just be another Route
, which should represent the nested routes that are appended to the parent route.
In this way, you are defining a sort of routing "tree," where each route entry can have many child routes defined recursively. This will be discussed in greater detail later in this chapter.
Furthermore, you should also use the URL parameter notation to declare :articleId
as a variable in the route. This allows you to pass values inside the route and then retrieve these values inside the component that is rendered.
Add these route definitions now:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {RouterModule, Routes} from '@angular/router'; import {RootComponent} from './root.component'; import {DefaultComponent} from './default.component'; import {ArticleComponent} from './article.component'; import {ArticleListComponent} from './article-list.component'; import {ArticleDetailComponent} from './article-detail.component'; const appRoutes:Routes = [ {path: 'article', component: ArticleComponent, children: [ {path: '', component: ArticleListComponent}, {path: ':articleId', component: ArticleDetailComponent} ] }, {path: '**', component: DefaultComponent}, ]; @NgModule({ imports: [ BrowserModule, RouterModule.forRoot(appRoutes) ], declarations: [ DefaultComponent, ArticleComponent, ArticleListComponent, ArticleDetailComponent, RootComponent ], bootstrap: [ RootComponent ] }) export class AppModule {}
You'll note that ArticleListComponent
is keyed by an empty string. This should make sense, as each of these routes are joined to their parent routes to create the full route. If you were to join each route in this tree with its ancestral path to get the full route, the route definition you've just created would have the following three entries:
/article => ArticleComponent ArticleListComponent /article/4 => ArticleComponent ArticleDetailComponent<articleId=4> /** => DefaultComponent
Note that in this case, the number of actual routes corresponds to the number of leaves of the URL tree since the article parent route will also map to the child article's + '' route. Depending on how you configure your route structure, the leaf/route parity will not always be the case.
With the routes being mapped to the child components, you can flesh out the child views. Starting with ArticleList
, create a repeater to generate the links to each of the child views:
[app/article-list.component.ts] import {Component} from '@angular/core'; @Component({ template: ` <h3>Article List</h3> <p *ngFor="let articleId of articleIds"> <a [routerLink]="articleId"> Article {{articleId}} </a> </p> ` }) export class ArticleListComponent { articleIds:Array<number> = [1,2,3,4,5]; }
Note that routerLink
is linking to the relative path of the detail view. Since the current path for this view is /article
, a relative routerLink
of 4 will navigate the application to /article/4
upon a click.
These links should work, but when you click on them, they will take you to the detail view that cannot display articleId
from the route since you have not extracted it yet.
Inside ArticleDetailComponent
, create a link that will take the user back to the article/
route. Since routes behave like directories, you can just use a relative path that will take the user up one level:
[app/article-detail.component.ts] import {Component} from '@angular/core'; @Component({ template: ` <h3>Article Detail</h3> <p>Showing article {{articleId}}</p> <a [routerLink]="'../'">Back up</a> ` }) export class ArticleDetailComponent { articleId:number; }
A crucial difference between Angular 1 and 2 is the reliance on Observable
constructs. In the context of routing, Angular 2 wields Observables
to encapsulate that routing occurs as a sequence of events and that values are produced at different states in these events and will be ready eventually.
More concretely, route params in Angular 2 are not exposed directly, but rather through an Observable
inside ActivatedRoute
. You can set Observer
on its params
Observable to extract the route params once they are available.
Inject the ActivatedRoute
interface and use the params Observable to extract articleId
and assign it to the ArticleDetailComponent
instance member:
[app/article-detail/article-detail.component.ts] import {Component} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; @Component({ template: ` <h3>Article Detail</h3> <p>Showing article {{articleId}}</p> <a [routerLink]="'../'">Back up</a> ` }) export class ArticleDetailComponent { articleId:number; constructor(private activatedRoute_: ActivatedRoute) { activatedRoute_.params .subscribe(params => this.articleId = params['articleId']); } }
With this, you should be able to see the articleId
parameter interpolated into ArticleDetailComponent
.
In this application, you have nested components, AppComponent
and ArticleComponent
, both of which contain RouterOutlet
. Angular is able to take the routing hierarchy you defined and apply it to the component hierarchy that it maps to. More specifically, for every Route
you define in your routing hierarchy, there should be an equal number of RouterOutlets
in which they can render.
To some, it will feel strange to need to extract the route params from an Observable
interface. If this solution feels a bit clunky to you, there are ways of tidying it up.
Recall that Angular has the ability to interpolate Observable
data directly into the template as it becomes ready. Especially since you should only ever expect the param Observable
to emit once, you can use it to insert articleId
into the template without explicitly setting an Observer
:
[app/article-detail.component.ts] import {Component} from '@angular/core'; import {ActivatedRoute } from '@angular/router'; @Component({ template: ` <h3>Article Detail</h3> <p>Showing article {{(activatedRoute.params | async).articleId}}</p> <a [routerLink]="'../'">Back up</a> ` }) export class ArticleDetailComponent { constructor(activatedRoute: ActivatedRoute) {} }
Even though this works perfectly well, using a private reference to an injected service directly into the template may feel a bit funny to you. A superior strategy is to grab a reference to the public Observable
interface you need and interpolate that instead:
[app/article-detail.component.ts] import {Component} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {ActivatedRoute, Params} from '@angular/router'; @Component({ template: ` <h3>Article Detail</h3> <p>Showing article {{(params | async).articleId}}</p> <a [routerLink]="'../'">Back up</a> ` }) export class ArticleDetailComponent { params:Observable<Params>; constructor(private activatedRoute_: ActivatedRoute) { this.params = activatedRoute_.params; } }