5

Routing Angular 2 Single Page Apps with the Component Router

 2 years ago
source link: http://brianyang.com/routing-angular-2-single-page-apps-with-the-component-router/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Routing Angular 2 Single Page Apps with the Component Router



We have been so eager here at Scotch to create a comprehensive guide on Angular 2 Component Router. Now that a reliable version (v3) has been released (although alpha), it's a good time then to discuss Component Router.

This article will serve as a guide to implementing routing in Angular 2 with a fully fleshed out example touching major aspects of routing including bootstrapping, configuration, parameterized routes, protecting routes, etc. In the past, we've looked at routing in Angular 1.x with ngRoute and UI-Router.

Take a look at what we'll be building today:

Getting Started: App Setup

Angular 2 uses TypeScript, so if you need a refresher on that, take a look at why TypeScript is your friend and our TypeScript tutorial series.

Before we get started and to save us some setup time, clone the Angular 2 QuickStart then we can build on top of that.

git clone https: //github.com/angular/quickstart scotch-ng-router

The seed already has end to end tools to enable you start building Angular 2 apps. It also comes with all Angular 2 dependencies including angular/router so we can just pull the packages from npm:

npm install

We are more concerned with the app folder of our new project which is simply:

| -- - app | -- -- - main.ts# Bootstrapper | -- -- - app.component.ts# Base Component | -- -- - app.component.spec.ts# Base Test File

Testing is out of scope for this tutorial so we won't be making use of app.component.spec.ts, but we'll be writing up an article on Angular 2 testing shortly.

Routing requires a lot more files than the above so we will re-structure and organize our app units in folders:

| -- - app | -- -- - Cats | -- -- -- - cat - list.component.ts# Component | -- -- -- - cat - details.component.ts# Component | -- -- -- - cat.routes.ts# Component routes | -- -- - Dogs | -- -- -- - dog - list.component.ts# Component | -- -- -- - dog - details.component.ts# Component | -- -- -- - dog.routes.ts# Component routes | -- -- - app.component.ts# Base Component | -- -- - app.routes.ts# Base Route | -- -- - main.ts# Bootstrapper | -- -- - pets.service.ts# HTTP Service
for fetch API data | -- - index.html# Entry# Other helper files here

We are not so interested in how the app looks but it won't hurt to make our app look prettier than what the quickstart offers.

Material Design and Angular are very good friends so we could go ahead to make use of Material Design Lite (MDL). Grab MDL with npm:

npm install material - design - lite--save

Replace <link rel="stylesheet" href="styles.css"> in ./index.html with:

You can then add some custom styles in the styles.css:

/** ./style.css **/ /* Jumbotron */ .demo - layout - transparent {
    background: linear - gradient(rgba(0, 0, 255, 0.45), rgba(0, 0, 255, 0.45)),
    url('assets/scotch-dog1.jpg') center / cover;height: 400 px;
} /* Nav Bar */ .demo - layout - transparent.mdl - layout__header, .demo - layout - transparent.mdl - layout__drawer - button {
    background: rgba(0, 0, 0, 0.3);color: white;
} /* Header Text */ .header - text {
    text - align: center;
    vertical - align: middle;
    line - height: 250 px;
    color: white;
} /* Content Wrapper */ .container {
    width: 80 % ;margin: 0 auto;
}

Defining Routes

Let's get started with configuring a basic route and see how that goes. app.routes.ts holds the base route configuration and it does not exist yet so we need to create that now:

// ====== ./app/app.routes.ts ====== // Imports import { provideRouter, RouterConfig } from '@angular/router'; import { CatListComponent } from './cats/cat-list.component'; import { DogListComponent } from './dogs/dog-list.component'; // Route Configuration export const routes: RouterConfig = [ { path: 'cats', component: CatListComponent }, { path: 'dogs', component: DogListComponent } ]; // Export routes export const APP_ROUTER_PROVIDERS = [ provideRouter(routes) ];

We import what we need for the base route configuration from @angular/router and also some components we have yet to create.

We then define an array of routes which is of type RouterConfig then use provideRouter to export the routes so it can be injected in our application when bootstrapping. The above configuration is basically what it takes to define routes in Angular 2.

Creating Placeholder Components

The routes config file imports some components that we need to create. For now we just create them and flesh them out with minimal content, then we can build on them while we move on.

// ====== ./app/Cats/cat-list.component.ts ====== // Import component decorator import { Component } from '@angular/core'; @Component({ template: ` <h2>Cats</h2> <p>List of cats</p>` }) // Component class export class CatListComponent {}

Just a basic Angular 2 component with a decorator and a component class.

If the codes seem weird to you then you need to read an Angular 2 getting started guide. You can start here.

// ====== ./app/Dogs/dog-list.component.ts ====== // Import component decorator import { Component } from '@angular/core'; @Component({ template: ` <h2>Dogs</h2> <p>List of dogs</p>` }) // Component class export class DogListComponent {}

Bootstrapping Our Application

We are doing great work already in configuring our application. It's time to bootstrap our application in ./app/main.ts with the configured routes. Open main.ts and update with:

// ====== ./app/main.ts ====== import { bootstrap } from '@angular/platform-browser-dynamic'; // Import App Component to be bootstrapped import { AppComponent } from './app.component'; // Import configured routes import { APP_ROUTER_PROVIDERS } from './app.routes'; // Bootstrap app with configured routes bootstrap(AppComponent, [ APP_ROUTER_PROVIDERS ]) .catch(err => console.error(err));

What this code does is bootstrap our App while injecting our routes during the bootstrap process.

The Main AppComponent

The question is, where is our App? It is yet to be created and we will do that right now:

// ====== ./app/app.component.ts ====== import { Component } from '@angular/core'; // Import router directives import { ROUTER_DIRECTIVES } from '@angular/router'; @Component({ selector: 'my-app', template: ` <div class="demo-layout-transparent mdl-layout mdl-js-layout"> <header class="mdl-layout__header mdl-layout__header--transparent"> <div class="mdl-layout__header-row"> <!-- Title --> <span class="mdl-layout-title">Scotch Pets</span> <!-- Add spacer, to align navigation to the right --> <div class="mdl-layout-spacer"></div> <!-- Navigation with router directives--> <nav class="mdl-navigation"> <a class="mdl-navigation__link" [routerLink]="['/']">Home</a> <a class="mdl-navigation__link" [routerLink]="['/cats']">Cats</a> <a class="mdl-navigation__link" [routerLink]="['/dogs']">Dogs</a> </nav> </div> </header> <main class="mdl-layout__content"> <h1 class="header-text">We care about pets...</h1> </main> </div> <!-- Router Outlet --> <router-outlet></router-outlet> `, // Tell component to use router directives directives: [ROUTER_DIRECTIVES] }) // App Component class export class AppComponent {}

That is just a basic Angular 2 component we are staring at with just one new thing - ROUTER_DIRECTIVES. ROUTER_DIRECTIVES also lives in @angular/router and it's where RouterLink and RouterOutlet directives reside.

The RouterLink directive substitutes the normal href property and makes it easier to work with route links in Angular 2. It has the following syntax:

The RouterOutlet directive is used to display views for a given route. This is where templates of specific routes are loaded while we navigate:

One thing not to forget is to tell the component that ROUTER_DIRECTIVES is needed by listing it in the array of directives in the component decorator:

directives: [ROUTER_DIRECTIVES]

Angular makes it easy to make our SPA route URLs look indistinguishable form sever-served URLs. All we need to do is set base URL in the index.html:

Let's see how far we have gone by running the app:

We do not have an index route (/) and that will throw errors to the console. Ignore it (will fix that) and navigate to /cats or /dogs:

uSRlFaOZTl2SpPSaYsDu_initial.png

Going a Little Deeper

Yes, we have a functional route, but we all know that real-life applications require a bit more than a simple route. Real apps have index/home page routes for:

Let's take some time and have a look at some of these routing features.

Index/Home Page Route and Redirects

First and most important is to fix our index route. I can't think of any relevant information to put in the index route so what we can do is redirect to /dogs once the index route is hit.

// ====== ./app/app.routes.ts ====== // redirect for the home page // Route Configuration export const routes: RouterConfig = [ { path: '', redirectTo: '/dogs', pathMatch: 'full' }, { path: 'cats', component: CatListComponent }, { path: 'dogs', component: DogListComponent } ];

We just successfully killed two birds with one stone. We have an index route and we have also seen how we can redirect to another route. If you prefer to have a component to the index route, configure as follows:

// ====== ./app/app.routes.ts ====== // component for the index/home page // Route Configuration export const routes: RouterConfig = [ { path: '', component: HomeComponent // Remember to import the Home Component }, { path: 'cats', component: CatListComponent }, { path: 'dogs', component: DogListComponent } ];

When making a redirect it is important to tell the router how to match the URL. There are two options for that - full or prefix. full matches the URL as it is while prefix matches URL prefixed with the redirect path.

Route Parameters

This is a good time to add more features to the demo app by fetching list of pets from a remote server and retrieving each pet details with their ID. This will give us a chance to see how route parameters work.

Pet Service to Get Pet Data

It's a good practice to isolate heavy tasks from our controllers using services. A service is a data class provider that makes a request (not necessarily a HTTP call) for data when a component needs to make use of it:

// ====== ./app/pet.service.ts ====== // Imports import { Injectable } from '@angular/core'; import { Jsonp, URLSearchParams } from '@angular/http'; // Decorator to tell Angular that this class can be injected as a service to another class @Injectable() export class PetService { // Class constructor with Jsonp injected constructor(private jsonp: Jsonp) { } // Base URL for Petfinder API private petsUrl = 'http://api.petfinder.com/'; // Get a list if pets based on animal findPets(animal : string) { // End point for list of pets: // http://api.petfinder.com/pet.find?key=[API_KEY]&animal=[ANIMAL]&format=json&location=texas const endPoint = 'pet.find' // URLSearchParams makes it easier to set query parameters and construct URL // rather than manually concatenating let params = new URLSearchParams(); params.set('key', '[API_KEY]'); params.set('location', 'texas'); params.set('animal', animal); params.set('format', 'json'); params.set('callback', 'JSONP_CALLBACK'); // Return response return this.jsonp .get(this.petsUrl + endPoint, { search: params }) .map(response => <string[]> response.json().petfinder.pets.pet); } // get a pet based on their id findPetById(id: string) { // End point for list of pets: // http://api.petfinder.com/pet.find?key=[API_KEY]&animal=[ANIMAL]&format=json&location=texas const endPoint = 'pet.get' // URLSearchParams makes it easier to set query parameters and construct URL // rather than manually concatinating let params = new URLSearchParams(); params.set('key', '[API_KEY]'); params.set('id', id); params.set('format', 'json'); params.set('callback', 'JSONP_CALLBACK'); // Return response return this.jsonp .get(this.petsUrl + endPoint, { search: params }) .map(response => <string[]> response.json().petfinder.pet); } }

A Little Bit on HTTP and Dependency Injection

HTTP and DI are beyond the scope of this article (though coming soon) but a little explanation of what is going on won't cause us harm.

The class is decorated with an @Injectable decorator which tells Angular that this class is meant to be used as a provider to other components. Jsonp rather than HTTP is going to be used to make API request because of CORS so we inject the service into PetService.

The class has 3 members - a private property which just holds the base Url of the Petfinder API, a method to retrieve list of pets based on type and another method to get a pet by it's Id.

Injecting PetService

PetService was not built to run on it's own, rather we need to inject the service into our existing list components:

// Imports import { Component, OnInit } from '@angular/core'; import { PetService } from '../pet.service' import { Observable } from 'rxjs/Observable'; import { ROUTER_DIRECTIVES } from '@angular/router'; @Component({ template: ` <h2>Dogs</h2> <p>List of dogs</p> <ul class="demo-list-icon mdl-list"> <li class="mdl-list__item" *ngFor="let dog of dogs | async"> <span class="mdl-list__item-primary-content"> <i class="material-icons mdl-list__item-icon">pets</i> <a [routerLink]="['/dogs', dog.id.$t]">{{ dog.name.$t }}</a> </span> </li> </ul> `, // Providers providers: [PetService], // Directives directives: [ROUTER_DIRECTIVES] }) // Component class implementing OnInit export class DogListComponent implements OnInit { // Private property for binding dogs: Observable<string[]>; constructor(private petService: PetService) { } // Load data ones componet is ready ngOnInit() { // Pass retreived pets to the property this.dogs = this.petService.findPets('dog'); } }

The trailing .$t is as a result of the API structure and not and Angular thing so you do not have to worry about that

We are binding an observable type, dogs to the view and looping through it with the NgFor directive. The component class extends OnInit which when it's ngOnInit method is overridden, is called once the component loads.

MPAFiMxUTqmD6LJtIBvF_list.png

routerLink with Parameters

A VERY important thing to also note is the routerLink again. This time it does not just point to /dog but has a parameter added

CatListComponent looks quite exactly like DogListComponent so I will leave that to you to complete.

Details Components

The link from the list components points to a non-existing route. The route's component is responsible for retrieving specific pet based on an id. The first thing to do before creating these components is to make there routes. Back to the app.routes:

// ====== ./app/app.routes.ts ====== // Imports import { provideRouter, RouterConfig } from '@angular/router'; import { CatListComponent } from './cats/cat-list.component'; import { DogRoutes } from './dogs/dog.routes'; // Route Configuration export const routes: RouterConfig = [ { path: '', redirectTo: '/dogs', pathMatch: 'full' }, { path: 'cats', component: CatListComponent }, // Add dog routes form a different file ...DogRoutes ]; export const APP_ROUTER_PROVIDERS = [ provideRouter(routes) ];

For modularity, the dog-related routes have been moved to a different file and then we import and add it to the base route using the spread operator. Our dog.routes now looks like:

// ======= ./app/Dogs/dog.routes.ts ===== // Imports import { provideRouter, RouterConfig } from '@angular/router'; import { DogListComponent } from './dog-list.component'; import { DogDetailsComponent } from './dog-details.component'; // Route Configuration export const DogRoutes: RouterConfig = [ { path: 'dogs', component: DogListComponent }, { path: 'dogs/:id', component: DogDetailsComponent } ];

There is now a DogDetailsComponent as you can now see but the component is yet to be created. The component will receive id parameter form the URL and use the parameter to query the API for a pet:

// ====== ./app/Dogs/dog-details.component ====== // Imports import { Component, OnInit } from '@angular/core'; import { PetService } from '../pet.service' import { Observable } from 'rxjs/Observable'; import { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router'; import { Pet } from '../pet'; @Component({ template: ` <div *ngIf="dog"> <h2>{{dog.name.$t}}</h2> <img src="{{dog.media.photos.photo[3].$t}}"/> <p><strong>Age: </strong>{{dog.age.$t}}</p> <p><strong>Sex: </strong>{{dog.sex.$t}}</p> <p><strong>Description: </strong>{{dog.description.$t}}</p> </div> `, // Providers providers: [PetService], // Directives directives: [ROUTER_DIRECTIVES] }) // Component class implementing OnInit export class DogDetailsComponent implements OnInit { // Private properties for binding private sub:any; private dog:string[]; constructor(private petService: PetService, private route: ActivatedRoute) { } // Load data ones componet is ready ngOnInit() { // Subscribe to route params this.sub = this.route.params.subscribe(params => { let id = params['id']; // Retrieve Pet with Id route param this.petService.findPetById(id).subscribe(dog => this.dog = dog); }); } ngOnDestroy() { // Clean sub to avoid memory leak this.sub.unsubscribe(); } }

What is important to keep an eye on is that we are getting the Id form the route URL using ActivatedRoute. The Id is passed in to the PetService's findPetById method to fetch a single pet. It might seem like a lot of work of having to subscribe to route params but there is more to it. Subscribing this way makes it easier to unsubscribe ones we exit the component in ngOnDestroy thereby cutting the risks of memory leak.

CfgbwGGWSvKv3c52MtRT_detail.png

Now it's time to take it as a challenge to complete that of CatDetailsComponent and CatRoutes though you can find them in the demo if you get stuck.

Let's take a look at one more thing we can do with component router

Guarding and Authenticating Routes

Sometimes we might have sensitive information to protect from certain categories of users that have access to our application. We might not want a random user to create pets, edit pets or delete pets.

Showing them these views when they cannot make use of it does not make sense so the best thing to do is to guard them.

Angular has two guards:

  • CanActivate (access route if return value is true)
  • CanDeactivate (leave route if return value is false)

This is how we can make use of the guard:

import {
    CanActivate
} from '@angular/router';
export class AuthGuard implements CanActivate {
    canActivate() { // Imaginary method that is supposed to validate an auth token // and return a boolean return tokenExistsAndNotExpired(); } }

It's a class that implements router's CanActivate and overrides canActivate method. What you can do with with the service is supply as array value to canActivate property of route configuration:

{
    path: 'admin',
    component: PetAdmin,
    canActivate: [AuthGuard]
}

Wrap Up

There is room to learn a lot more on Angular Component Router but what we have seen is enough to guide you through what you need to start taking advantage of routing in an Angular 2 application.

More to come from Scotch and Scotch-School on Angular 2, do watch out so you don't miss.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK