Parameterised Routes
Learning Objectives
-
How to configure parameterised routes in our route definition object.
-
How components can be notified with what the parameter values are when the URL gets visited.
-
How to have optional parameters in routes.
Configuration
Sometimes we need part of the path in one or more of our routes (the URLs) to be a variable, a common example of this is an ID.
Let’s say we have a blog and each article in our blog has an ID, the URLs for each blog article might look like
Copy/blog/1 /blog/2 /blog/3 /blog/4 and so on...
Now we could write a route for each article like so:
Copyconst routes: Routes = [
{ path: 'blog/1', component: Blog1Component },
{ path: 'blog/2', component: Blog2Component },
{ path: 'blog/3', component: Blog3Component },
{ path: 'blog/4', component: Blog4Component },
];
But a better solution is to have one route with one component called BlogComponent
and pass to the BlogComponent
the number part of the URL.
That’s called a parameterised route and we would implement it like so:
Copyconst routes: Routes = [
{ path: 'blog/:id', component: BlogComponent } (1)
];
1 | The path has a variable called id , we know it’s a variable since it begins with a colon : |
A path can have any number of variables as long as they all start with :
and have different names.
Non-parameterised routes always take priority over parameterised routes, so in the below config:
Copyconst routes: Routes = [
{ path: 'blog/:id', component: BlogComponent },
{ path: 'blog/moo', component: MooComponent },
];
If we visited /blog/moo
we would show MooComponent
even though it matches the path for the first blog/:id
route as well.
Important
Activated Route
So how do we pass into BlogComponent
the value of the id variable? If we visited /blog/1
how does BlogComponent
know the id
is 1
and therefore to show the appropriate article.
To do that we use something called an ActivatedRoute
.
We import it first and then inject it into the constructor of BlogComponent
. It exposes an Observable which we can subscribe to, like so:
Copyimport {ActivatedRoute} from "@angular/router";
.
.
.
constructor(private route: ActivatedRoute) {
this.route.params.subscribe( params => console.log(params) );
}
Now if we navigated to /blog/1
the number 1
would get emitted on the observable and this would get printed to the console as:
Copy{ id: 1 }
Example
For the rest of this lecture we are going to continue building the iTunes search app we’ve been working on in the other lectures.
As we perform searches with the app the URL doesn’t change. Therefore if I refresh the page after I perform a search then I lose all my search results — the state of my app is lost.
Let’s turn the search route into a parameterised route where the search term is in the URL, so if I refresh the page it will perform the same search and get us back to where we were.
Parameterised Route Configuration
We begin with just adding a variable called term
to our route configuration, like so:
Copy{path: 'search/:term', component: SearchComponent},
But if we now load the app and try to go to the search page nothing happens, the URL changes to /search
but we are still shown the HomeComponent
.
The reason for that is that now our URL /search
doesn’t match a route so we are falling back to the catch-all route which just shows the HomeComponent
.
To match our new parameterised route we would have to navigate to something like /search/U2
. We would actually need to pass a parameter to match that route.
So to support both /search
and /search/U2
we need two routes in our configuration, like so:
Copy{path: 'search', component: SearchComponent},
{path: 'search/:term', component: SearchComponent},
Now our app supports both /search
and /search/U2
.
Activated Route
Next let’s import ActivatedRoute
and inject it into the constructor of our SearchComponent
, like so:
Copyconstructor(private itunes:SearchService,
private route: ActivatedRoute) {
this.route.params.subscribe( params => console.log(params));
}
We subscribe for updates to the params of the currently active route and just print them out to the console.
Now if we navigate to /search/U2
we get { term: 'U2' }
printed in our console as expected but we are not actually performing a search for U2
. To do that all we need to do is call doSearch(…​)
from the activated route subscribe callback, like so:
Copyconstructor(private itunes:SearchService,
private route: ActivatedRoute) {
this.route.params.subscribe( params => this.doSearch(params['term'])); (1)
}
1 | We call doSearch and pass in the term parameter from the URL. |
Now when we visit /search/U2
the SearchComponent
is notified via the ActivatedRoute
and we perform the query and show the user the search results for U2
.
But now if we now search for another term, for example Foo
, we get the results we expect but the URL doesn’t change, it’s still /search/U2
.
Important
When using routing if some part of the state of your application is in the URL then you need to update your application by navigating to the URL.
That way the URL matches the state of your app and if you bookmarked or shared the URL then visiting it again would get you back to the same state.
In our case what this means is that when the user submits a search, instead of calling doSearch(…​)
we instead navigate to the appropriate search URL and then let the ActivatedRoute
service notify the SearchComponent
the route changed and let that perform the search.
This way the URL changes every time we do a search.
First we make the click handler on the Search button point to another function called onSearch(…​)
instead of doSearch(…​)
.
Copy<button type="button"
class="btn btn-primary"
(click)="onSearch(search.value)">
Search
</button>
And in our onSearch
function we just navigate to the correct search URL.
CopyonSearch(term:string) {
this.router.navigate(['search', term]); (1)
}
1 | The second parameter to the link params array is a variable, it’s the search term the user typed in. |
Now when we search Foo
the URL changes to /search/Foo
the SearchComponent
gets notified through the ActivatedRoute
service and performs the search, therefore the URL and the state of our application are now in sync.
Optional Parameters
Going back to the route config and the solution of having two routes configured, one for when there is a search term and another for when there isn’t a search term.
Copy{path: 'search', component: SearchComponent},
{path: 'search/:term', component: SearchComponent}
Another way to think about the above is that the variable term
is optional. It might be present and it might not and we want the app to function correctly in either case.
Angular has a mechanism to support optional params with only one route defined.
Firstly let’s get rid of the second route with the fixed term variable, leaving us with just one route to support our search like so:
Copyconst routes:Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'find', redirectTo: 'search'},
{path: 'home', component: HomeComponent},
{path: 'search', component: SearchComponent},
{path: '**', component: HomeComponent}
];
Next in the onSearch(…​)
function instead of navigating to a route with the fixed term param, we can instead pass in an object containing whatever params we want, like so:
CopyonSearch(term:string) {
this.router.navigate(['search', {term: term}]);
}
Now when we perform a search the URL changes, but it doesn’t quite looks like something we are used to, instead of /search/U2
we see /search;term=U2
Note
We can see that the param term
still gets passed along via the ActivatedRoute
as {term: "U2"}
.
Copy/search;term=U2;foo=moo
Will print out
CopyObject {term: "U2", foo: "moo"}
And also if we pass nothing we get an empty object printed out to the console
Copy/search
Will print out
CopyObject {}
So now the term
param is optional, but since it’s optional it sometimes can be blank. Let’s add a little bit of defensive coding in our subscribe function so we don’t call doSearch
if no term has been provided, like so:
Copyconstructor(private itunes: SearchService,
private route: ActivatedRoute,
private router: Router) {
this.route.params.subscribe(params => {
console.log(params);
if (params['term']) { (1)
this.doSearch(params['term'])
}
});
}
1 | Only call doSearch(…​) if term has a value. |
Now if we visit /search
we are shown a blank screen ready for a search.
-
If we perform a search for e.g.
U2
the URL changes to/search;term=U2
-
If we refresh the page then the application performs a search for
U2
again.
Summary
With parameterised routes we can support variable paths in our routes.
Angular also supports optional routes via passing in an object to the navigate function and the matrix URL notation.
So far we’ve only shown how we can output one component on the page depending on the route. In the next lecture we are going to show how we can have nested routes and output multiple different components on the page depending on the URL.
Listing
Copyimport { NgModule, Component, Injectable } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { ReactiveFormsModule, FormControl, FormsModule } from "@angular/forms";
import {
HttpClientJsonpModule,
HttpClientModule,
HttpClient
} from "@angular/common/http";
import { Routes, RouterModule, Router, ActivatedRoute } from "@angular/router";
class SearchItem {
constructor(
public name: string,
public artist: string,
public link: string,
public thumbnail: string,
public artistId: string
) {}
}
@Injectable()
class SearchService {
apiRoot: string = "https://itunes.apple.com/search";
results: SearchItem[];
constructor(private http: HttpClient) {
this.results = [];
}
search(term: string) {
return new Promise((resolve, reject) => {
this.results = [];
let apiURL = `${this.apiRoot}?term=${term}&media=music&limit=20`;
this.http
.jsonp(apiURL, "callback")
.toPromise()
.then(
res => {
// Success
this.results = res.results.map(item => {
return new SearchItem(
item.trackName,
item.artistName,
item.trackViewUrl,
item.artworkUrl30,
item.artistId
);
});
resolve();
},
msg => {
// Error
reject(msg);
}
);
});
}
}
@Component({
selector: "app-search",
template: `<form class="form-inline">
<div class="form-group">
<input type="search"
class="form-control"
placeholder="Enter search string"
#search>
</div>
<button type="button"
class="btn btn-primary"
(click)="onSearch(search.value)">
Search
</button>
</form>
<hr />
<div class="text-center">
<p class="lead"
*ngIf="loading">Loading...</p>
</div>
<div class="list-group">
<a href="#"
class="list-group-item list-group-item-action"
*ngFor="let track of itunes.results">
<img src="{{track.thumbnail}}">
{{ track.name }} <span class="text-muted">by</span> {{ track.artist }}
</a>
</div>
`
})
class SearchComponent {
private loading: boolean = false;
constructor(
private itunes: SearchService,
private route: ActivatedRoute,
private router: Router
) {
this.route.params.subscribe(params => {
console.log(params);
if (params["term"]) {
this.doSearch(params["term"]);
}
});
}
doSearch(term: string) {
this.loading = true;
this.itunes.search(term).then(_ => (this.loading = false));
}
onSearch(term: string) {
this.router.navigate(["search", { term: term }]);
}
}
@Component({
selector: "app-home",
template: `
<div class="jumbotron">
<h1 class="display-3">iTunes Search App</h1>
</div>
`
})
class HomeComponent {}
@Component({
selector: "app-header",
template: `<nav class="navbar navbar-light bg-faded">
<a class="navbar-brand"
[routerLink]="['home']">iTunes Search App
</a>
<ul class="nav navbar-nav">
<li class="nav-item"
[routerLinkActive]="['active']">
<a class="nav-link"
[routerLink]="['home']">Home
</a>
</li>
<li class="nav-item"
[routerLinkActive]="['active']">
<a class="nav-link"
[routerLink]="['search']">Search
</a>
</li>
</ul>
</nav>
`
})
class HeaderComponent {
constructor(private router: Router) {}
goHome() {
this.router.navigate([""]);
}
goSearch() {
this.router.navigate(["search"]);
}
}
@Component({
selector: "app",
template: `
<app-header></app-header>
<div class="m-t-1">
<router-outlet></router-outlet>
</div>
`
})
class AppComponent {}
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "find", redirectTo: "search" },
{ path: "home", component: HomeComponent },
{ path: "search", component: SearchComponent },
{ path: "**", component: HomeComponent }
];
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule,
FormsModule,
HttpClientJsonpModule,
HttpClientModule,
RouterModule.forRoot(routes, { useHash: true })
],
declarations: [AppComponent, SearchComponent, HomeComponent, HeaderComponent],
bootstrap: [AppComponent],
providers: [SearchService]
})
class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
Caught a mistake or want to contribute to the book? Edit this page on GitHub!

Advanced JavaScript
This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.
Copy[🌲,🌳,🌴].push(🌲)If you find my courses useful, please consider planting a tree on my behalf to combat climate change. Just $4.50 will pay for 25 trees to be planted in my name. Plant a tree!