Angular Routing and Navigation with jQWidgets

Routing & Navigation

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.

Overview

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.

The Basics

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="/">

Router imports

The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular core. It is in its own library package, @angular/router. Import what you need from it as you would from any other Angular package.
import { RouterModule, Routes } from '@angular/router';

Configuration

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.

src/app/app.module.ts

 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));  
  }  
 }  
        

Router outlet

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

Router links

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.

CustomAnchorComponent

import { Component } from '@angular/core';  
 @Component({  
   selector: 'custom-anchor',  
   template: '<a [routerLink]="[\'/hero\', id]">{{id}}</a>'  
 })  
 export class CustomAnchorComponent{  
      id: Number  
 }  
        

jqxDomService

      
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();	  
  }
}
        

Router state

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.

Activated route

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 Observable of the route path(s), represented as an array of strings for each part of the route path.

data

An Observable that contains the data object provided for the route. Also contains any resolved values from the resolve guard.

paramMap

An Observable that contains a map(Router ParamMap) of the required and optional parameters specific to the route. The map supports retrieving single and multiple values from the same parameter.

queryParamMap

An Observable that contains a map of the query parameters available to all routes. The map supports retrieving single and multiple values from the query parameter.

fragment

An Observable of the URL fragment available to all routes.

outlet

The name of the RouterOutlet used to render the route. For an unnamed outlet, the outlet name is primary.

routeConfig

The route configuration used for the route that contains the origin path.

parent

The route's parent ActivatedRoute when this route is a child route.

firstChild

Contains the first ActivatedRoute in the list of this route's child routes.

children

Contains all the child routes activated under the current route.

Router events

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 Router lazy loads a route configuration.

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.

The sample application

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.

package.json

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

_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": {}
  }
}