#native_company# #native_desc#
#native_cta#

Route Configuration


Learning Objectives

  • How to configure and setup the Angular router.

  • How to handle redirects and catch-all routes in Angular.

Components

To explain routing in Angular we start with an app very similar to the one we created in the section on Http.

We have a SearchComponent which lets us perform searches using the iTunes API, we are using JSONP to bypass CORS and Promises instead of Observables to keep things simple.

In addition we’ve also created a HeaderComponent with a selector of app-header and two menu items, Home and Search. We also have a HomeComponent with selector app-home which just shows a simple welcome message.

In one configuration, the main AppComponent renders these two components like so:

<app-header></app-header>
<div class="m-t-1">
  <app-search></app-search>
</div>

Note

m-t-1 above is a class from Twitter Bootstrap which adds a top margin to the element so we can clearly distinguish the different elements.

The above is the structure we want when the user navigates to /search, this renders the page with the header and the search component.

app search

If we changed the template to be:

<app-header></app-header>
<div class="m-t-1">
  <app-home></app-home>
</div>

This renders the page with the header and the home component, we want this structure when the user navigates to the root / URL.

app home

Routes & RouterModule

Our goal with routing is to have the HomeComponent rendered when the URL is / and the SearchComponent shown when the URL is /search

First we need to setup some imports, like so:

import {Routes, RouterModule} from "@angular/router";

The mapping of URLs to Components we want displayed on the page is done via something called a Route Configuration, at it’s core it’s just an array which we can define like so:

const routes: Routes = [
 { path: '', component: HomeComponent },
 { path: 'search', component: SearchComponent }
];
  • The path property describes the URL this route will handle.

  • The component property is the name of the component we want to display when the URL in the browser matches this path.

Note

Routes is a TypeScript type of Route[], an array of individual Route instances.

We then install these routes into our application by importing RouterModule.forRoot(routes) into our NgModule, like so:

@NgModule({
	imports: [
	  .
	  .
		RouterModule.forRoot(routes, {useHash: true})
	]
	.
	.
	.
})
class AppModule { }

Important

We’ll go through the meaning of the {useHash: true} argument when we cover Path Location Strategies later on in this section. For now just know that this prepends /# to all of our URLs, so our root URL would be /#/ and our search URL would be /#/search.

RouterOutlet Directive

We’ve configured our application so if the user navigates to /#/search we want the SearchComponent shown or if they navigate to the root URL /#/ then we want the HomeComponent shown.

But where exactly do we want the component shown?

We need to add a directive called router-outlet somewhere in our template HTML. This directive tells Angular where it should insert each of those components in the route, we’ll add ours to the AppComponent, like so:

<app-header></app-header>
<div class="m-t-1">
  <router-outlet></router-outlet> (1)
</div>
1 We place <router-outlet> where we want the component inserted.

Now if we run the application and visit the root URL we are shown the HomeComponent and if we visit the /search URL we are shown the SearchComponent.

Redirects

There are a few more ways to configure our routes, for example we might like to change our routes to add some redirects like so:

const routes:Routes = [
	{path: '', redirectTo: 'home', pathMatch: 'full'}, (1)
	{path: 'find', redirectTo: 'search'}, (1)
	{path: 'home', component: HomeComponent},
	{path: 'search', component: SearchComponent}
];
1 The redirectTo property describes the path we want to redirect this user to if they navigate to this URL.

Now if the user visits the root (empty) URL they are redirected to /home instead.

Note

For the special case of an empty URL we also need to add the pathMatch: 'full' property so Angular knows it should be matching exactly the empty string and not partially the empty string.

We’ve also added a redirect from /find to /search, since this isn’t empty we don’t need to add the pathMatch property.

Catch-All Route

We can also add a catch-all route by using the path **, if the URL doesn’t match any of the other routes it will match this route.

const routes:Routes = [
	{path: '', redirectTo: 'home', pathMatch: 'full'},
	{path: 'find', redirectTo: 'search'},
	{path: 'home', component: HomeComponent},
	{path: 'search', component: SearchComponent},
	{path: '**', component: HomeComponent} (1)
];
1 If nothing matches we show the HomeComponent

Now if we navigate to /foo it will show us the welcome message.

In our example above we are just showing the HomeComponent but normally we might show an error 404 page.

Summary

A route in our application is defined by a mapping of a URL to a component or a redirect to another URL.

We can create an array of Routes and then install them in our application by importing them into our NgModule using RouterModule.forRoot(routes).

In this lecture we’ve shown how we can configure routes and manually type in the different URLs in the address bar to make the application render different components depending on which URL the user visits.

Next we’ll show how you can navigate between these different routes in Angular without having to manually type the URL into the browser address bar.

Listing

Listing 1. main.ts
import { 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 [routerLink]="['/artist', track.artistId]"
     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-artist-track-list",
  template: `
<ul class="list-group">
	<li class="list-group-item"
	    *ngFor="let track of tracks">
		<img src="{{track.artworkUrl30}}">
		<a target="_blank"
		   href="{{track.trackViewUrl}}">{{ track.trackName }}
		</a>
	</li>
</ul>
 `
})
class ArtistTrackListComponent {
  private tracks: any[];

  constructor(private http: HttpClient, private route: ActivatedRoute) {
    this.route.parent.params.subscribe(params => {
      this.http
        .jsonp(
          `https://itunes.apple.com/lookup?id=${
            params["artistId"]
          }&entity=song`,
          "callback"
        )
        .toPromise()
        .then(res => {
          console.log(res);
          this.tracks = res.results.slice(1);
        });
    });
  }
}

@Component({
  selector: "app-artist-album-list",
  template: `<ul class="list-group">
	<li class="list-group-item"
	    *ngFor="let album of albums">
		<img src="{{album.artworkUrl60}}">
		<a target="_blank"
		   href="{{album.collectionViewUrl}}">{{ album.collectionName }}
		</a>
	</li>
</ul>
 `
})
class ArtistAlbumListComponent {
  private albums: any[];

  constructor(private http: HttpClient, private route: ActivatedRoute) {
    this.route.parent.params.subscribe(params => {
      this.http
        .jsonp(
          `https://itunes.apple.com/lookup?id=${
            params["artistId"]
          }&entity=album`,
          "callback"
        )
        .toPromise()
        .then(res => {
          console.log(res);
          this.albums = res.results.slice(1);
        });
    });
  }
}

@Component({
  selector: "app-artist-video-list",
  template: `<ul class="list-group">
	<li class="list-group-item"
	    *ngFor="let video of videos">
		<img src="{{video.artworkUrl60}}">
		<a target="_blank"
		   href="{{video.previewUrl}}">{{ video.trackName }}
		</a>
	</li>
</ul>
 `
})
class ArtistMusicVideoListComponent {
  private videos: any[];

  constructor(private http: HttpClient, private route: ActivatedRoute) {
    this.route.parent.params.subscribe(params => {
      this.http
        .jsonp(
          `https://itunes.apple.com/lookup?id=${
            params["artistId"]
          }&entity=musicVideo`,
          "callback"
        )
        .toPromise()
        .then(res => {
          console.log(res);
          this.videos = res.results.slice(1);
        });
    });
  }
}

@Component({
  selector: "app-artist",
  template: `<div class="card">
  <div class="card-block">
    <h4>{{artist?.artistName}} <span class="tag tag-default">{{artist?.primaryGenreName}}</span></h4>
    <hr />
    <footer>
      <ul class="nav nav-pills">
        <li class="nav-item">
          <a class="nav-link"
             [routerLinkActive]="['active']"
             [routerLink]="['./tracks']">Tracks
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link"
             [routerLinkActive]="['active']"
             [routerLink]="['./albums']">Albums
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link"
             [routerLinkActive]="['active']"
             [routerLink]="['./videos']">Videos
          </a>
        </li>
      </ul>
    </footer>
  </div>
</div>

<div class="m-t-1">
  <router-outlet></router-outlet>
</div>
 `
})
class ArtistComponent {
  private artist: any;

  constructor(private http: HttpClient, private route: ActivatedRoute) {
    this.route.params.subscribe(params => {
      this.http
        .jsonp(
          `https://itunes.apple.com/lookup?id=${params["artistId"]}`,
          "callback"
        )
        .toPromise()
        .then(res => {
          console.log(res);
          this.artist = res.results[0];
          console.log(this.artist);
        });
    });
  }
}

@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: "home", component: HomeComponent },
  { path: "find", redirectTo: "search" },
  { path: "search", component: SearchComponent },
  {
    path: "artist/:artistId",
    component: ArtistComponent,
    children: [
      { path: "", redirectTo: "tracks", pathMatch: "full" },
      { path: "tracks", component: ArtistTrackListComponent },
      { path: "albums", component: ArtistAlbumListComponent },
      { path: "videos", component: ArtistMusicVideoListComponent }
    ]
  },
  { path: "**", component: HomeComponent }
];

@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    FormsModule,
    HttpClientJsonpModule,
    HttpClientModule,
    RouterModule.forRoot(routes, { useHash: true })
  ],
  declarations: [
    AppComponent,
    SearchComponent,
    HomeComponent,
    HeaderComponent,
    ArtistAlbumListComponent,
    ArtistTrackListComponent,
    ArtistMusicVideoListComponent,
    ArtistComponent
  ],
  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.

Level up your JavaScript now!