Angular 2 – Router links, click handlers, routing parameters

As with last week’s post this post is going to cover multiple topics related to while creating a contact detail page to go along with the existing contact list page that is part of the ASP.NET Basics repo, but this time using Angular 2 instead of Aurelia. The code before any changes can be found here. If you are using the sample application keep in mind all the changes in this post take place in the Angular project.

Creating a detail view and view model

Create contactdetail.component.html in ClientApp\app\components\contacts. This view that will be used to display all the details of a specific contact. It will also link back to the contact list. The following image shows the folder structure after the view and view model have been added.

View

The following is the full contents of the view.

<h1>Contact Details</h1>
<hr />
<div *ngIf="contact">
    <dl class="dl-horizontal">
        <dt>ID</dt>
        <dd>{{contact.id}}</dd>
        <dt>Name</dt>
        <dd>{{contact.name}}</dd>
        <dt>Address</dt>
        <dd>{{contact.getAddress()}}</dd>
        <dt>Phone</dt>
        <dd>{{contact.phone}}</dd>
        <dt>Email</dt>
        <dd>{{contact.email}}</dd>
    </dl>
</div>
<a routerLink="/contact-list">Back to List</a>
<hr />

*ngIf is adding/removing the associated div based on a contact being set or not.

{{value}} is Angular’s one-way binding syntax. For more details check out the docs.

<a routerLink=”/contact-list”> is using Angular to generate a link back to the contact list component.

View model

For the view model add contactdetail.component.ts in the ClientApp\app\components\contacts folder which is the same folder used for the view.

Make not of the imports needed to make this view model work. To start off Contact is needed to define what the definition of a contact is and ContactService is being used to load the data for a specific contact.

Angular’s core is imported to allow the view model to set as a component using @Component decorator as well as to all implementation of OnInit lifecycle hook.  Angular’s router is being used in the ngOnInit function to allow access the parameters of the route that caused the route to be triggered using this.route.params.

The switchMap operator from reactive extensions is used to map the id from the route parameters to a new observable that has the result of this.contactService.getById.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import 'rxjs/add/operator/switchMap';
import { Contact } from './contact';
import { ContactService } from './contact.service';

@Component({
    selector: 'contactdetail',
    template: require('./contactdetail.component.html'),
    providers: [ContactService]
})
export class ContactDetailComponent implements OnInit {
    contact: Contact;

    constructor(private route: ActivatedRoute,
                private router: Router,
                private contactService: ContactService) { }

    ngOnInit(): void {
        this.route.params
            .switchMap((params: Params) => 
                   this.contactService.getById(params['id']))
            .subscribe((contact :Contact) => this.contact = contact);
    }
}

Adding get by ID to the Contact Service

The existing ContactService doesn’t provide a function to get a contact by a specific ID so one needs to be added.

The following calls the API in the Contacts project and uses the result to create an instance of a  Contact as a promise which is returned to the caller.

getById(id: string): Promise<Contact> {
    return this.http.get(this.baseUrl + id)
        .toPromise()
        .then(response => response.json())
        .then(contact => new Contact(contact))
        .catch(error => console.log(error));
}

The base URL was also moved to a class level variable so that it could be shared.

Add a route with a parameter

To add the new contact detail to the list of routes that the application handles open app.module.ts in the ClientApp/app folder. First, add an import at the top of the file for the new component.

import { ContactDetailComponent } from './components/contacts/contactdetail.component';

Next, add the ContactDetailComponent to the declarations array of the @NgModule decorator.

declarations: [
    AppComponent,
    NavMenuComponent,
    CounterComponent,
    FetchDataComponent,
    ContactListComponent,
    ContactDetailComponent,
    HomeComponent
]

Finally, add the new route to the RouteModule in the imports section of the @NgModule decorator.

RouterModule.forRoot([
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: 'home', component: HomeComponent },
    { path: 'counter', component: CounterComponent },
    { path: 'fetch-data', component: FetchDataComponent },
    { path: 'contact-list', component: ContactListComponent },
    { path: 'contact-detail/:id', component: ContactDetailComponent },
    { path: '**', redirectTo: 'home' }
])

path is the pattern used to match URLs. In addition, parameters can be used in the form of :parameterName. The above route will handle requests for http://baseurl/contact-detail/{id} where {id} is an ID of a contact. As demonstrated above in the ngOnInit function of the view model route.params can be used to access route parameters.

component is used to locate the view/view model that goes with the route.

Integrating the detail view with the contact list

The contact list view and view model needed the following changes to support the contact detail page.

View model

A variable was added for the ID of the select contact was as well as a onSelect function which takes a contact and gets called when a contact is selected. The following is the fully contact list view model.

import { Component, OnInit } from '@angular/core';
import { Contact } from './contact';
import { ContactService } from './contact.service';

@Component({
    selector: 'contactlist',
    template: require('./contactlist.component.html'),
    providers: [ContactService]
})
export class ContactListComponent implements OnInit {
    contacts: Contact[];
    selectedContactId: number = null;

    constructor(private contactService: ContactService) { }

    ngOnInit(): void {
        this.contactService.getAll()
            .then(contacts => this.contacts = contacts);
    }

    onSelect(contact) {
        this.selectedContactId = contact.id;
    }
}

The changes to the view model were not required to add the contact detail page, but are used show how to set up a click handler the view side. In the future, the selected contact will come in handy when the list and details were shown at the same time.

View

The amount of data being displayed was reduced to just ID and name. A column was added with a link to the details page. The following is the full view.

<h1>Contact List</h1>

<p *ngIf="!contacts"><em>Loading...</em></p>

<table class="table" *ngIf="contacts">
    <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let contact of contacts">
            <td>{{contact.id}}</td>
            <td>{{contact.name}}</td>
            <td><a [routerLink]="['/contact-detail', contact.id]" (click)="onSelect(contact)">Details</a></td>
        </tr>
    </tbody>
</table>

(click)=”onSelect(contact)” will cause the onSelect function of the view model to be called with the related contact when the associated element is clicked.

[routerLink]=”[‘/contact-detail’, contact.id]” use the router to create a line to the contact details page passing the contact ID as a parameter.

As a reminder of how to use a route’s parameter here is the ngOnInit function from the contact detail view model.

ngOnInit(): void {
    this.route.params
        .switchMap((params: Params) => this.contactService.getById(params['id']))
        .subscribe((contact :Contact) => this.contact = contact);
}
 Wrapping up

As with the related Aurelia post, there are a lot of topics covered in this post, but they are all related to the process of adding a new page and route to the application.

The code including all the changes for this post can be found here.

 

Leave a Comment

Your email address will not be published. Required fields are marked *

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