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.

Lets say we have a blog and each article in our blog has an ID, the URLs for each blog article might look like

/blog/1
/blog/2
/blog/3
/blog/4
and so on...

Now we could write a route for each article like so:

const 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:

const 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:

const 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

Non-parameterised routes take precedence over parameterised routes.

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:

import {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:

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

Lets 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:

{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:

{path: 'search', component: SearchComponent},
{path: 'search/:term', component: SearchComponent},

Now our app supports both /search and /search/U2.

Activated route

Next lets import ActivatedRoute and inject it into the constructor of our SearchComponent, like so:

constructor(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:

constructor(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(…​).

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

onSearch(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 params

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.

{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 lets get rid of the second route with the fixed term variable, leaving us with just one route to support our search like so:

const 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:

onSearch(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

This is called Matrix URL notation, a series of optional key=value pairs separated with the semicolon ; character.

We can see that the param term still gets passed along via the ActivatedRoute as {term: "U2"}.

In-fact if we added some other params to the mix via the URL they also get printed to the console, like so:

/search;term=U2;foo=moo

Will print out

Object {term: "U2", foo: "moo"}

And also if we pass nothing we get an empty object printed out to the console

/search

Will print out

Object {}

So now the term param is optional, but since it’s optional it sometimes can be blank. Lets 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:

  constructor(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

Listing 1. script.ts
import {NgModule, Component, Injectable} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {JsonpModule, Jsonp, Response} from '@angular/http';
import {ReactiveFormsModule, FormControl, FormsModule} from '@angular/forms';
import {Routes, RouterModule} from "@angular/router";
import {Observable} from 'rxjs';
import 'rxjs/add/operator/toPromise';
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 jsonp: Jsonp) {
    this.results = [];
  }

  search(term: string) {
    return new Promise((resolve, reject) => {
      this.results = [];
      let apiURL = `${this.apiRoot}?term=${term}&media=music&limit=20&callback=JSONP_CALLBACK`;
      this.jsonp.request(apiURL)
          .toPromise()
          .then(
              res => { // Success
                this.results = res.json().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,
    JsonpModule,
    RouterModule.forRoot(routes, {useHash: true})
  ],
  declarations: [
    AppComponent,
    SearchComponent,
    HomeComponent,
    HeaderComponent
  ],
  bootstrap: [AppComponent],
  providers: [SearchService]
})
class AppModule {
}

platformBrowserDynamic().bootstrapModule(AppModule);

Learn Angular 5 For FREE

I've released my 700 page Kick Starter funded Angular 5 book for FREE