The Angular Router enables navigation from one view to the next as users perform application tasks.
This guide covers the router's primary features, illustrating them through the evolution of a small application based on the https://angular.io/guide/router and using our jQWidgets Angular Components.
The browser is a familiar model of application navigation:
Enter a URL in the address bar and the browser navigates to a corresponding page. Click links on the page and the browser navigates to a new page. Click the browser's back and forward buttons and the browser navigates backward and forward through the history of pages you've seen. The Angular Router ("the router") borrows from this model. It can interpret a browser URL as an instruction to navigate to a client-generated view. It can pass optional parameters along to the supporting view component that help it decide what specific content to present. You can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. You can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well.
This guide proceeds in phases, marked by milestones, starting from a simple two-pager and building toward a modular, multi-view design with child routes.
An introduction to a few core router concepts will help orient you to the details that follow.
<base href>
Most routing applications should add a <base> element to the index.html as the first child in the <head> tag to tell the router how to compose navigation URLs. If the app folder is the application root, as it is for the sample application, set the href value exactly as shown here.
<base href="/">
import { RouterModule, Routes } from '@angular/router';
A routed Angular application has one singleton instance of the Router service. When the browser's URL changes, that router looks for a corresponding Route from which it can determine the component to display.
A router has no routes until you configure it. The following example creates four route definitions, configures the router via the RouterModule.forRoot method, and adds the result to the AppModule's imports array.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { ComposeMessageComponent } from './compose-message.component'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { PageNotFoundComponent } from './not-found.component'; import { DialogService } from './dialog.service'; import { jqxDomService } from './jqwidgets-dom.service'; import { CustomAnchorComponent } from './custom-anchor.component'; import { jqxGridModule} from 'jqwidgets-ng/jqxgrid'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, LoginRoutingModule, jqxGridModule, AppRoutingModule, BrowserAnimationsModule ], declarations: [ AppComponent, ComposeMessageComponent, LoginComponent, PageNotFoundComponent, CustomAnchorComponent ], entryComponents: [CustomAnchorComponent], providers: [ DialogService, jqxDomService ], bootstrap: [ AppComponent ] }) export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { console.log('Routes: ', JSON.stringify(router.config, undefined, 2)); } }
Given this configuration, when the browser URL for this application becomes /heroes, the router matches that URL to the route path /heroes and displays the HeroListComponent after a RouterOutlet that you've placed in the host view's HTML.
<router-outlet></router-outlet> <!-- Routed views go here -->
Now you have routes configured and a place to render them, but how do you navigate? The URL could arrive directly from the browser address bar. But most of the time you navigate as a result of some user action such as the click of an anchor tag. Consider the following template:
import { jqxDomService } from './jqwidgets-dom.service'; import { Observable } from 'rxjs/Observable'; import { Component, OnInit } from '@angular/core'; import { Hero, HeroService } from './heroes/hero.service'; import { CustomAnchorComponent } from './custom-anchor.component'; @Component({ selector: 'app-root', template: ` <h1 class="title">Angular Router</h1> <jqxGrid #grid [autoheight]="true" [theme]="'metro'" [columns]=columns> <tr> <th>Id</th> <th>Name</th> </tr> <tr *ngFor="let hero of heroes"> <td> {{hero.id}}</td> <td> {{hero.name}}</td> </tr> </jqxGrid> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <router-outlet></router-outlet> <router-outlet name="popup"></router-outlet> ` }) export class AppComponent { heroes: Hero[] = []; constructor(private jqxDomService: jqxDomService, private service: HeroService) { let heroes = this.service.getHeroes()["value"]; for(var i = 0; i < heroes.length; i++) { this.heroes.push(heroes[i]); } } ngOnInit() { } columns: any[] = [ { text: 'Id', datafield: 'Id', createwidget: (row: number, column: any, value: string, htmlElement: HTMLElement): void => { let container = document.createElement('div'); htmlElement.appendChild(container); let result = this.jqxDomService.loadComponent(CustomAnchorComponent, container); (<CustomAnchorComponent>result.componentRef.instance).id = parseInt(value); }, initwidget: (row: number, column: any, value: any, htmlElement: HTMLElement): void => { } }, { text: 'Name', datafield: 'Name' } ]; }
The RouterLink directives on the anchor tags give the router control over those elements. The navigation paths are fixed, so you can assign a string to the routerLink (a "one-time" binding). Had the navigation path been more dynamic, you could have bound to a template expression that returned an array of route link parameters (the link parameters array). The router resolves that array into a complete URL. The RouterLinkActive directive on each anchor tag helps visually distinguish the anchor for the currently selected "active" route. The router adds the active CSS class to the element when the associated RouterLink becomes active. You can add this directive to the anchor or to its parent element.
Another important thing in the template is the jqxGrid related code. We define the jqxGrid tag and fill it with rows by using the *ngFor
Angular directive. Within the AppComponent
class, look at the createwidget
implementation. By using the jqxDomService
, we dynamically load Angular Component into the jqxGrid. The name of the Angular component is CustomAnchorComponent
and its purpose is to render an Anchor tag with RouterLink pointing to an item in the Hero array.
import { Component } from '@angular/core'; @Component({ selector: 'custom-anchor', template: '<a [routerLink]="[\'/hero\', id]">{{id}}</a>' }) export class CustomAnchorComponent{ id: Number }
import { Injectable, Injector, EmbeddedViewRef, ComponentFactoryResolver, ApplicationRef } from '@angular/core'; @Injectable() export class jqxDomService { componentRef: any; constructor( private componentFactoryResolver: ComponentFactoryResolver, private appRef: ApplicationRef, private injector: Injector ) { } loadComponent(component: any, ownerElement: any) { // 1. Create a component reference from the component const componentRef = this.componentFactoryResolver .resolveComponentFactory(component) .create(this.injector, ownerElement); // 2. Attach component to the appRef so that it's inside the ng component tree this.appRef.attachView(componentRef.hostView); // 3. Get DOM element from component const domElement = (componentRef.hostView as EmbeddedViewRef) .rootNodes[0] as HTMLElement; if (ownerElement) { ownerElement.appendChild(domElement); } this.componentRef = componentRef; return {componentRef: componentRef, domElement: domElement } } destroy() { this.appRef.detachView(this.componentRef.hostView); this.componentRef.destroy(); } }
After the end of each successful navigation lifecycle, the router builds a tree of ActivatedRoute objects that make up the current state of the router. You can access the current RouterState from anywhere in the application using the Router service and the routerState property. Each ActivatedRoute in the RouterState provides methods to traverse up and down the route tree to get information from parent, child and sibling routes.
The route path and parameters are available through an injected router service called the ActivatedRoute. It has a great deal of useful information including:
Property | Description |
---|---|
url
|
An |
data
|
An |
paramMap
|
An |
queryParamMap
|
An |
fragment
|
An |
outlet
|
The name of the |
routeConfig
|
The route configuration used for the route that contains the origin path. |
parent
|
The route's parent |
firstChild
|
Contains the first |
children
|
Contains all the child routes activated under the current route. |
During each navigation, the Router emits navigation events through the Router.events property. These events range from when the navigation starts and ends to many points in between. The full list of navigation events is displayed in the table below.
Router Event | Description |
---|---|
NavigationStart
|
An event triggered when navigation starts. |
RoutesRecognized
|
An event triggered when the Router parses the URL and the routes are recognized. |
RouteConfigLoadStart
|
An event triggered before the |
RouteConfigLoadEnd
|
An event triggered after a route has been lazy loaded. |
NavigationEnd
|
An event triggered when navigation ends successfully. |
NavigationCancel
|
An event triggered when navigation is canceled. This is due to a Route Guard returning false during navigation. |
NavigationError
|
An event triggered when navigation fails due to an unexpected error. |
This guide describes development of a multi-page routed sample application. Along the way, it highlights design decisions and describes key features of the router such as:
The full source for the final version of the app can be downloaded from router.zip
The sample application in action Imagine an application that helps the Hero Employment Agency run its business. Heroes need work and the agency finds crises for them to solve.
The application has three main feature areas:
Once the app starts, you will see this view
Select one hero and the app takes you to a hero editing screen.
Edit the Name and you will see this screen
Alter the name. Click the "Back" button and the app returns to the heroes list which displays the changed hero name. Notice that the name change took effect immediately. Had you clicked the browser's back button instead of the "Back" button, the app would have returned you to the heroes list as well. Angular app navigation updates the browser history as normal web navigation does.
{ "name": "angular-io-example", "version": "1.0.0", "private": true, "description": "Example project from an angular.io guide.", "scripts": { "ng": "ng", "build": "ng build --base-href ./ --prod", "start": "ng serve", "test": "ng test", "lint": "tslint ./src/**/*.ts -t verbose", "e2e": "ng e2e" }, "keywords": [], "author": "", "license": "https://www.jqwidgets.com/license/", "dependencies": { "@angular/animations": "~5.0.0", "@angular/common": "~5.0.0", "@angular/compiler": "~5.0.0", "@angular/compiler-cli": "~5.0.0", "@angular/core": "~5.0.0", "@angular/forms": "~5.0.0", "@angular/http": "~5.0.0", "@angular/platform-browser": "~5.0.0", "@angular/platform-browser-dynamic": "~5.0.0", "@angular/platform-server": "~5.0.0", "@angular/router": "~5.0.0", "@angular/upgrade": "~5.0.0", "jqwidgets-ng": "^9.1.1", "angular-in-memory-web-api": "~0.5.0", "core-js": "^2.4.1", "rxjs": "^5.5.0", "zone.js": "^0.8.4" }, "devDependencies": { "@angular/cli": "1.6.5", "@types/jasmine": "~2.8.0", "@types/jasminewd2": "^2.0.3", "@types/node": "^6.0.45", "jasmine-core": "~2.8.0", "jasmine-spec-reporter": "^4.2.1", "karma": "^1.3.0", "karma-chrome-launcher": "^2.0.0", "karma-cli": "^1.0.1", "karma-coverage-istanbul-reporter": "^1.3.3", "karma-jasmine": "^1.0.2", "karma-jasmine-html-reporter": "^0.2.2", "karma-phantomjs-launcher": "^1.0.2", "lodash": "^4.16.2", "phantomjs-prebuilt": "^2.1.7", "protractor": "~5.1.0", "ts-node": "^3.3.0", "tslint": "^3.15.1", "typescript": "2.4.2" }, "repository": {} }
Include jQWidgets styles in _angular-cli.json
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular.io-example" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css", "../node_modules/jqwidgets-ng/jqwidgets/styles/jqx.base.css", "../node_modules/jqwidgets-ng/jqwidgets/styles/jqx.metro.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }