Angular 2 child routing and components

This article shows how Angular 2 child routing can be set up together with Angular 2 components. An Angular 2 component can contain it’s own routing, which makes it easy to reuse or test the components in an application.

Code: Angular 2 app host with ASP.NET Core

2017.01.07: Updated to Angular 2.4.1, Webpack 2.2.0-rc.3
2016.11.07: Updated to Angular 2.1.1, Webpack 2, AoT, treeshaking
2016.07.03: Updated to Angular 2 rc4
2016.06.26: Updated to Angular 2 rc3, new Angular 2 routing
2016.06.21: Updated to Angular 2 rc2
2016.05.07: Updated to Angular 2 rc1

An Angular 2 app bootstraps a single main module and the routing is usually defined in this module. To use Angular 2 routing, the ‘@angular/router’ npm package is imported. The child routing is set up using “…” and is defined in a type Routes const. Is this demo, the DATA_RECORDS_ROUTES is imported and the route definitions for the child component(s) are defined in the dataeventrecords.routes.ts file. The main application routing does not need to know anything about this.

The main routes are defined in the app.routes.ts

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

import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecureFilesComponent } from './securefile/securefiles.component';

import { DATA_RECORDS_ROUTES } from './dataeventrecords/dataeventrecords.routes';

import { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';

const appRoutes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'home', component: HomeComponent },
    { path: 'Forbidden', component: ForbiddenComponent },
    { path: 'Unauthorized', component: UnauthorizedComponent },
    { path: 'securefile/securefiles', component: SecureFilesComponent },
    ...DATA_RECORDS_ROUTES,
];

export const routing = RouterModule.forRoot(appRoutes);

This is then added to the application app.module.ts using the routing const which was defined in the routing typescript file. The child components are also added here. The child components with its routes could also be packed as a module and only the module would need to be included then.

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { Configuration } from './app.constants';
import { routing } from './app.routes';
import { HttpModule, JsonpModule } from '@angular/http';

import { SecurityService } from './services/SecurityService';
import { SecureFileService } from './securefile/SecureFileService';
import { DataEventRecordsService } from './dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './dataeventrecords/models/DataEventRecord';

import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecureFilesComponent } from './securefile/securefiles.component';

import { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        routing,
        HttpModule,
        JsonpModule
    ],
    declarations: [
        AppComponent,
        ForbiddenComponent,
        HomeComponent,
        UnauthorizedComponent,
        SecureFilesComponent,
        DataEventRecordsListComponent,
        DataEventRecordsCreateComponent,
        DataEventRecordsEditComponent
    ],
    providers: [
        SecurityService,
        SecureFileService,
        DataEventRecordsService,
        Configuration
    ],
    bootstrap:    [AppComponent],
})

export class AppModule {}

The routes can then be used in the app without anymore definitions.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { Configuration } from './app.constants';

import { SecurityService } from './services/SecurityService';
import { SecureFileService } from './securefile/SecureFileService';
import { DataEventRecordsService } from './dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './dataeventrecords/models/DataEventRecord';

import { ForbiddenComponent } from './forbidden/forbidden.component';
import { HomeComponent } from './home/home.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { SecureFilesComponent } from './securefile/securefiles.component';

import { DataEventRecordsListComponent } from './dataeventrecords/dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords/dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords/dataeventrecords-edit.component';

import './app.component.css';

@Component({
    selector: 'my-app',
    templateUrl: 'app.component.html'
})

export class AppComponent implements OnInit {

    constructor(public securityService: SecurityService) {  
    }

    ngOnInit() {
        console.log("ngOnInit _securityService.AuthorizedCallback");

        if (window.location.hash) {
            this.securityService.AuthorizedCallback();
        }      
    }

    public Login() {
        console.log("Do login logic");
        this.securityService.Authorize(); 
    }

    public Logout() {
        console.log("Do logout logic");
        this.securityService.Logoff();
    }
}

The corresponding HTML template for the main component contains the router-outlet directive. This is where the child routing content will be displayed. “[routerLink]” bindings can be used to define routing links.

<div class="container" style="margin-top: 15px;">
    <!-- Static navbar -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <button aria-controls="navbar" aria-expanded="false" data-target="#navbar" data-toggle="collapse" class="navbar-toggle collapsed" type="button">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a [routerLink]="['/dataeventrecords']" class="navbar-brand"><img src="images/damienbod.jpg" height="40" style="margin-top:-10px;" /></a>
            </div>
            <div class="navbar-collapse collapse" id="navbar">
                <ul class="nav navbar-nav">
                    <li><a [routerLink]="['/dataeventrecords']">DataEventRecords</a></li>
                    <li><a [routerLink]="['/dataeventrecords/create']">Create DataEventRecord</a></li>
                    <li><a [routerLink]="['/securefile/securefiles']">Secured Files Download</a></li>

                    <li><a class="navigationLinkButton" *ngIf="!securityService.IsAuthorized" (click)="Login()">Login</a></li>
                    <li><a class="navigationLinkButton" *ngIf="securityService.IsAuthorized" (click)="Logout()">Logout</a></li>
              
                </ul>
            </div><!--/.nav-collapse -->
        </div><!--/.container-fluid -->
    </nav>

    <router-outlet></router-outlet>

</div>

A child component in angular 2 can also contain its own routes. These can be defined in a dataeventrecords.routes.ts file, and then added to the parent routing definition using the … notation.

import { Routes, RouterModule } from '@angular/router';
import { DataEventRecordsListComponent } from './dataeventrecords-list.component';
import { DataEventRecordsCreateComponent } from './dataeventrecords-create.component';
import { DataEventRecordsEditComponent } from './dataeventrecords-edit.component';

export const DATA_RECORDS_ROUTES: Routes = [
    {
        path: 'dataeventrecords',
        children: [
            { path: '', redirectTo: 'list'},
            {
                path: 'create',
                component: DataEventRecordsCreateComponent
            },
            {
                path: 'edit/:id',
                component: DataEventRecordsEditComponent
            },
            {
                path: 'list',
                component: DataEventRecordsListComponent,
            }
        ]
    }
];

The list component is used as the default component inside the DataEventRecord component. This gets a list of DataEventRecord items using the DataEventRecordsService service and displays them in the UI using its HTML template.

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { SecurityService } from '../services/SecurityService';
import { DataEventRecord } from './models/DataEventRecord';

@Injectable()
export class DataEventRecordsService {

    private actionUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        this.actionUrl = `${_configuration.Server}api/DataEventRecords/`;   
    }

    private setHeaders() {

        console.log("setHeaders started");

        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');

        var token = this._securityService.GetToken();
        if (token !== "") {
            let tokenValue = 'Bearer ' + token;
            console.log("tokenValue:" + tokenValue);
            this.headers.append('Authorization', tokenValue);
        }
    }

    public GetAll = (): Observable<DataEventRecord[]> => {
        this.setHeaders();
        let options = new RequestOptions({ headers: this.headers, body: '' });

        return this._http.get(this.actionUrl, options).map(res => res.json());
    }

    public GetById = (id: number): Observable<DataEventRecord> => {
        this.setHeaders();
        return this._http.get(this.actionUrl + id, {
            headers: this.headers,
            body: ''
        }).map(res => res.json());
    }

    public Add = (itemToAdd: any): Observable<Response> => {       
        this.setHeaders();
        return this._http.post(this.actionUrl, JSON.stringify(itemToAdd), { headers: this.headers });
    }

    public Update = (id: number, itemToUpdate: any): Observable<Response> => {
        this.setHeaders();
        return this._http
            .put(this.actionUrl + id, JSON.stringify(itemToUpdate), { headers: this.headers });
    }

    public Delete = (id: number): Observable<Response> => {
        this.setHeaders();
        return this._http.delete(this.actionUrl + id, {
            headers: this.headers
        });
    }

}

The template for the list component uses the “[routerLink]” so that each item can be opened and updated using the edit child component.

<div class="col-md-12" *ngIf="securityService.IsAuthorized" >
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <table class="table">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Timestamp</th>
                    </tr>
                </thead>
                <tbody>
                    <tr style="height:20px;" *ngFor="let dataEventRecord of DataEventRecords" >
                        <td>
                            <a *ngIf="securityService.HasAdminRole" href="" [routerLink]="['/dataeventrecords/edit/' + dataEventRecord.Id]" >{{dataEventRecord.Name}}</a>
                            <span *ngIf="!securityService.HasAdminRole">{{dataEventRecord.Name}}</span>
                        </td>
                        <td>{{dataEventRecord.Timestamp}}</td>
                        <td><button (click)="Delete(dataEventRecord.Id)">Delete</button></td>
                    </tr>
                </tbody>
            </table>

        </div>
    </div>
</div>

The update or edit component uses the ActivatedRoute so that the id of the item can be read from the URL. This is then used to get the item from the ASP.NET Core MVC service using the DataEventRecordsService service. This is implemented in the ngOnInit and the OnDestroy function.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { SecurityService } from '../services/SecurityService';

import { DataEventRecordsService } from '../dataeventrecords/DataEventRecordsService';
import { DataEventRecord } from './models/DataEventRecord';

@Component({
    selector: 'dataeventrecords-edit',
    templateUrl: 'dataeventrecords-edit.component.html'
})

export class DataEventRecordsEditComponent implements OnInit, OnDestroy   {

    private id: number;
    public message: string;
    private sub: any;
    public DataEventRecord: DataEventRecord;

    constructor(
        private _dataEventRecordsService: DataEventRecordsService,
        public securityService: SecurityService,
        private _route: ActivatedRoute,
        private _router: Router
    ) {
        this.message = "DataEventRecords Edit";
    }
    
    ngOnInit() {     
        console.log("IsAuthorized:" + this.securityService.IsAuthorized);
        console.log("HasAdminRole:" + this.securityService.HasAdminRole);

        this.sub = this._route.params.subscribe(params => {
            let id = +params['id']; // (+) converts string 'id' to a number
            if (!this.DataEventRecord) {
                this._dataEventRecordsService.GetById(id)
                    .subscribe(data => this.DataEventRecord = data,
                    error => this.securityService.HandleError(error),
                    () => console.log('DataEventRecordsEditComponent:Get by Id complete'));
            } 
        });      
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }

    public Update() {
        // router navigate to DataEventRecordsList
        this._dataEventRecordsService.Update(this.id, this.DataEventRecord)
            .subscribe((() => console.log("subscribed")),
            error => this.securityService.HandleError(error),
            () => this._router.navigate(['/dataeventrecords']));
    }
}

The component loads the data from the service async, which means this item can be null or undefined. Because of this, it is important that *ngIf is used to check if it exists, before using it in the input form. The (click) event calls the Update function, which updates the item on the ASP.NET Core server.

<div class="col-md-12" *ngIf="securityService.IsAuthorized" >
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <div  *ngIf="DataEventRecord">
                <div class="row" >
                    <div class="col-xs-2">Id</div>
                    <div class="col-xs-6">{{DataEventRecord.Id}}</div>
                </div>

                <hr />
                <div class="row">
                    <div class="col-xs-2">Name</div>
                    <div class="col-xs-6">
                        <input type="text" [(ngModel)]="DataEventRecord.Name" style="width: 100%" />
                    </div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">Description</div>
                    <div class="col-xs-6">
                        <input type="text" [(ngModel)]="DataEventRecord.Description" style="width: 100%" />
                    </div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">Timestamp</div>
                    <div class="col-xs-6">{{DataEventRecord.Timestamp}}</div>
                </div>
                <hr />
                <div class="row">
                    <div class="col-xs-2">
                        <button (click)="Update()">Update</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

The new routing with child routing in Angular2 makes it better or possible to separate or group your components as required and easy to test. These could be delivered as separate modules or whatever.

Links

https://angular.io/docs/ts/latest/guide/router.html

http://www.codeproject.com/Articles/1087605/Angular-typescript-configuration-and-debugging-for

https://auth0.com/blog/2016/01/25/angular-2-series-part-4-component-router-in-depth/

https://github.com/johnpapa/angular-styleguide

https://mgechev.github.io/angular2-style-guide/

https://github.com/mgechev/codelyzer

https://toddmotto.com/component-events-event-emitter-output-angular-2

http://blog.thoughtram.io/angular/2016/03/21/template-driven-forms-in-angular-2.html

http://raibledesigns.com/rd/entry/getting_started_with_angular_2

https://toddmotto.com/transclusion-in-angular-2-with-ng-content

http://www.bennadel.com/blog/3062-creating-an-html-dropdown-menu-component-in-angular-2-beta-11.htm

http://asp.net-hacker.rocks/2016/04/04/aspnetcore-and-angular2-part1.html

4 comments

  1. […] Angular 2 child routing and components – Damian Bowden […]

  2. Without your update today (well, yesterday now), I’d be lost and super frustrated (even w/ the Angular.io updates). Issues resolved. Thanks!

    1. thanks, greetings Damien

  3. gunnarsireus · · Reply

    Hello Damien.
    In your solution damienbod/dotnet-template-angular, can you please implement startDateIndex in SampleDataController?

    [HttpGet(“[action]”)]
    public IEnumerable WeatherForecasts(int startDateIndex)
    {
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
    DateFormatted = DateTime.Now.AddDays(index + startDateIndex).ToString(“d”),
    TemperatureC = rng.Next(-20, 55),
    Summary = Summaries[rng.Next(Summaries.Length)]
    });
    }

    Please also add the buttons “Previous” and “Next” on the fetch-data view.

    I have come this far:
    In nav-menu.componenet.html:

    Home

    Counter

    Fetch data

    However, I cannot figure out how to inject startDateIndex in fetch-data.component.ts. Can you give a hint about that? I have tried this:

    import { Component, Inject } from ‘@angular/core’;
    import { HttpClient } from ‘@angular/common/http’;

    @component({
    selector: ‘app-fetch-data’,
    templateUrl: ‘./fetch-data.component.html’,
    })
    export class FetchDataComponent {
    public forecasts: WeatherForecast[];

    constructor(http: HttpClient, @Inject(‘BASE_URL’) baseUrl: string, @Inject(‘startDateIndex’) startDateIndex:number) {
    http.get(baseUrl + ‘api/SampleData/WeatherForecasts/’+ startDateIndex).subscribe(result => {
    this.forecasts = result;
    }, error => console.error(error));
    }
    }

    Thanks in advance, Gunnar Sireus

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: