This is a follow up article on a previous post where I talked about using
Consuming a RESful API with Angular 2 using angular-cli. In this article I will build an Angular 2 application that is more complete with routing and CRUD services that interact with a remote Web API service.
To proceed with this tutorial, it is assumed that you have the following applications already installed on your computer:
The first step is to create an application named ng2-flintstones. Type the following within a working directory in a terminal windows:
ng new ng2-flintones
Change to the newly created directory that houses your application with:
cd ng2-flintones
Start the application with this command:
ng serve
Point your browser to
http://localhost:4200. You should see this web page:
Open the
ng2-flinstones folder in Visual Studio Code. Open
app.component.ts and modify the class definition so that it looks like this:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
PersonId = 1;
FirstName = "Fred";
LastName = "Flintstone";
Occupation = "Mining Manager";
Gender = "M";
Picture = "http://flintstones.zift.ca/images/flintstone/fred.png";
}
Modify app.component.html so that it has the following markup:
<p>Person ID: {{PersonId}}</p>
<p>First Name: {{FirstName}}</p>
<p>Last Name: {{LastName}}</p>
<p>Occupation: {{Occupation}}</p>
<p>Gender: {{Gender}}</p>
<p>Picture: <img src="{{Picture}}" alt="{{FirstName}} {{LastName}}" /></p>
Refresh your browser. You will be able to see the data that was initialized in the
AppComponent class.
Instead of displaying literal data, let us create a
CartoonCharacter class. Enter the following command in a terminal window while in the root folder of your application:
ng generate class CartoonCharacter
The above command produces a CartoonCharacter class file named cartoon-character.ts. Modify CartoonCharacter so that it looks like this:
export class CartoonCharacter {
PersonId: number;
FirstName: string;
LastName: string;
Occupation: string;
Gender: string;
Picture: string;
}
Replace the contents of app.component.ts with the following code:
import { Component } from '@angular/core';
import {CartoonCharacter} from './cartoon-character';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
character: CartoonCharacter = {
PersonId: 1,
FirstName: "Fred",
LastName: "Flintstone",
Occupation: "Mining Manager",
Gender: "M",
Picture: "http://flintstones.zift.ca/images/flintstone/fred.png",
};
}
Now that we introduced a class, we can change app.component.html so that it contains the following markup:
<p>Person ID: {{ character.PersonId}}</p>
<p>First Name: {{ character.FirstName}}</p>
<p>Last Name: {{ character.LastName}}</p>
<p>Occupation: {{ character.Occupation}}</p>
<p>Gender: {{ character.Gender}}</p>
<p>Picture: <img src="{{ character.Picture}}"
alt="{{ character.FirstName}} {{ character.LastName}}" /></p>
Refresh the web page. It should appear just like it did previously. The only difference is that we rendered data from an instance of a CartoonCharacter class.
Add the following HTML to the bottom of the app.component.html file:
<div>
<p><label>First Name: </label><input value="{{ character.FirstName}}" placeholder="FirstName"></p>
<p><label>Last Name: </label><input value="{{ character.LastName}}" placeholder="LastName"></p>
<p><label>Occupation: </label><input value="{{ character.Occupation}}" placeholder="Occupation"></p>
<p><label>Gender: </label><input value="{{ character.Gender}}" placeholder="M or F"></p>
<p><label>Picture: </label><input value="{{ character.Picture}}" placeholder="Picture"></p>
</div>
Refresh the page and you will notice that some input boxes were added. Unfortunately, if you edit a data item in the text box it does not change above. We will fix that by implementing 2-way binding using [(ngModel)]. Replace app.component.html with the following:
<p>Person ID: {{character.PersonId}}</p>
<p>First Name: {{character.FirstName}}</p>
<p>Last Name: {{character.LastName}}</p>
<p>Occupation: {{character.Occupation}}</p>
<p>Gender: {{character.Gender}}</p>
<p>Picture: <img src="{{character.Picture}}"
alt="{{character.FirstName}} {{character.LastName}}" /></p>
<div>
<p><label>First Name: </label><input [(ngModel)]="character.FirstName" placeholder="FirstName"></p>
<p><label>Last Name: </label><input [(ngModel)]="character.LastName" placeholder="LastName"></p>
<p><label>Occupation: </label><input [(ngModel)]="character.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="character.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="character.Picture" placeholder="Picture"></p>
</div>
View page. When you change any of the fields the other data changes.
Create a new folder under
app named
data. In that folder create a file named
dummy-data.ts with following content:
import {CartoonCharacter} from '../cartoon-character';
export const DUMMY_DATA: CartoonCharacter[] = [
{"PersonId":1,"FirstName":"Fred","LastName":"Flintstone","Occupation":"Mining Manager","Gender":"M","Picture":"http://flintstones.zift.ca/images/flintstone/fred.png"},
{"PersonId":2,"FirstName":"Barney","LastName":"Rubble","Occupation":"Mining Assistant","Gender":"M","Picture":"http://flintstones.zift.ca/images/flintstone/barney.png"},
{"PersonId":3,"FirstName":"Betty","LastName":"Rubble","Occupation":"Nurse","Gender":"F","Picture":"http://flintstones.zift.ca/images/flintstone/betty.png"},
{"PersonId":4,"FirstName":"Wilma","LastName":"Flintstone","Occupation":"Teacher","Gender":"F","Picture":"http://flintstones.zift.ca/images/flintstone/wilma.png"},
{"PersonId":5,"FirstName":"Bambam","LastName":"Rubble","Occupation":"Baby","Gender":"M","Picture":"http://flintstones.zift.ca/images/flintstone/bambam.png"},
{"PersonId":6,"FirstName":"Pebbles","LastName":"Flintstone","Occupation":"Baby","Gender":"F","Picture":"http://flintstones.zift.ca/images/flintstone/pebbles.png"},
{"PersonId":7,"FirstName":"Dino","LastName":"Flintstone","Occupation":"Pet","Gender":"F","Picture":"http://flintstones.zift.ca/images/flintstone/dino.png"}
]
Add to
app.component.ts the following import statement:
import {DUMMY_DATA} from './data/dummy-data';
Add the following instance variable to the AppComponent class:
characters = DUMMY_DATA;
There is a form module that needs to be added to our project because we will be displaying and accepting data from a form. Therefore, find app.module.ts and add to it the following import code at the top of the file:
import { FormsModule} from '@angular/forms'
Also, add FormsModule to the imports array in the same app.module.ts file.
In order to display a collection of cartoon-characters, replace app.component.html with the following markup:
<table *ngIf="characters" border="1">
<thead>
<tr>
<th>Person ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Occupation</th>
<th>Gender</th>
<th>Picture</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of characters; let i=index; ">
<td>{{c.PersonId}}</td>
<td>{{c.FirstName}}</td>
<td>{{c.LastName}}</td>
<td>{{c.Occupation}}</td>
<td>{{c.Gender}}</td>
<td><img src="{{c.Picture}}"
alt="{{c.FirstName}} {{c.LastName}}" /></td>
</tr>
</tbody>
</table>
<p>Person ID: {{character.PersonId}}</p>
<p>First Name: {{character.FirstName}}</p>
<p>Last Name: {{character.LastName}}</p>
<p>Occupation: {{character.Occupation}}</p>
<p>Gender: {{character.Gender}}</p>
<p>Picture: <img src="{{character.Picture}}"
alt="{{character.FirstName}} {{character.LastName}}" /></p>
<div>
<p><label>First Name: </label><input [(ngModel)]="character.FirstName" placeholder="FirstName"></p>
<p><label>Last Name: </label><input [(ngModel)]="character.LastName" placeholder="LastName"></p>
<p><label>Occupation: </label><input [(ngModel)]="character.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="character.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="character.Picture" placeholder="Picture"></p>
</div>
Now when you view the page once more you will notice that whenever you change a data item in the textbox it also changes above.
We want the user to select a cartoon-character from our list, and have the selected character appear in the details view. Modify the opening
<tr> tag by inserting an Angular event binding to its click event, like this:
<tr *ngFor="let c of characters; let i=index;" (click)="onSelect(c)">
In the
AppComponent class, replace the
character declaration with:
selected: CartoonCharacter;
Now add an
onSelect method to
AppComponent that sets the selected property to the character the user clicked on.
onSelect(character: CartoonCharacter): void {
this.selected= character;
}
We will be showing the selected character details in our template. Now, it is still referring to the old
character property. Let’s fix the template to bind to the new
selected property.
Hide the empty detail with *ngIf
When our app loads we see a list of characters, but a cartoon-character is not selected. The
selected property is undefined. That’s why we'll see the following error in the browser’s console:
EXCEPTION: TypeError: Cannot read property 'PersonId' of undefined in [null]
Wrap the HTML cartoon character detail content of our template with a
<div>. Then we add the
*ngIf built-in directive and set it to the
selected property of our component like this:
<div *ngIf="selected">
<p>Person ID: {{selected.PersonId}}</p>
<p>First Name: {{selected.FirstName}}</p>
<p>Last Name: {{selected.LastName}}</p>
<p>Occupation: {{selected.Occupation}}</p>
<p>Gender: {{selected.Gender}}</p>
<p>Picture: <img src="{{selected.Picture}}" alt="{{selected.FirstName}} {{selected.LastName}}" /></p>
<div>
<p><label>First Name: </label><input [(ngModel)]="selected.FirstName" placeholder="FirstName"></p>
<p><label>Last Name: </label><input [(ngModel)]="selected.LastName" placeholder="LastName"></p>
<p><label>Occupation: </label><input [(ngModel)]="selected.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="selected.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="selected.Picture" placeholder="Picture"></p>
</div>
</div>
CSS
Let us change the background color for a row when it is selected. Add the following CSS into the
app.component.css file:
.selected {
background-color: #CFD8DC !important;
color: darkblue;
}
In
app.component.html, add the following to the
<tr> tag that manages the iteration:
[class.selected]="c === selected"
The
<tr> tag would look like this:
<tr *ngFor="let c of characters; let i=index;" (click)="onSelect(c)" [class.selected]="c === selected">
Separating the character details
Add a new details component with the following terminal command:
ng generate component CharacterDetail
This produces the following files:
src\app\character-detail\character-detail.component.css
src\app\character-detail\character-detail.component.html
src\app\character-detail\character-detail.component.spec.ts
src\app\character-detail\character-detail.component.ts
In
character-detail.component.ts, modify the import statement so that it also imports “
Input” as follows:
import { Component, OnInit, Input } from '@angular/core';
Move the whole
<div> block that contains detail information from the
app.component.html file to the
character-detail.component.html file. Do a search and replace from “selected.” to “character.”. The contents of
character-detail.component.html will look like this:
<div *ngIf="character">
<p>Person ID: {{character.PersonId}}</p>
<p>First Name: {{character.FirstName}}</p>
<p>Last Name: {{character.LastName}}</p>
<p>Occupation: {{character.Occupation}}</p>
<p>Gender: {{character.Gender}}</p>
<p>Picture: <img src="{{character.Picture}}" alt="{{character.FirstName}} {{character.LastName}}" /></p>
<div>
<p><label>First Name: </label><input [(ngModel)]="character.FirstName" placeholder="FirstName"></p>
<p><label>Last Name: </label><input [(ngModel)]="character.LastName" placeholder="LastName"></p>
<p><label>Occupation: </label><input [(ngModel)]="character.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="character.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="character.Picture" placeholder="Picture"></p>
</div>
</div>
Add the cartoon-character property
Add a
character property to the
CharacterDetailComponent component class:
@Input()
character: CartoonCharacter;
Also, import the
CartoonCharacter class with the following import statement.
import {CartoonCharacter} from '../cartoon-character';
The
CharacterDetailComponent class must be told what cartoon-character to display by the parent
AppComponent class. This will be placed in the
<app-character-detail> tag as follows:
<app-character-detail [character]="selected"></app-character-detail>
We will update app.component.html with this later. Meantime, annotate the
character property with the
@Input decorator that we imported earlier.
Refresh the AppComponent
Open
app.component.html and replace the
<div> tag that is responsible for displaying selected data with the following:
<app-character-detail [character]="selected"></app-character-detail>
The
app.component.html file should now look like this:
<table *ngIf="characters" border="1">
<thead>
<tr>
<th>Person ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Occupation</th>
<th>Gender</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of characters; let i=index;" (click)="onSelect(c)" [class.selected]="c === selected">
<td>{{c.PersonId}}</td>
<td>{{c.FirstName}}</td>
<td>{{c.LastName}}</td>
<td>{{c.Occupation}}</td>
<td>{{c.Gender}}</td>
</tr>
</tbody>
</table>
<app-character-detail [character]="selected"></app-character-detail>
Check the web application in your browser and make sure that it works and behaves just as it did before we separated out the details about our characters.
Creating a CartoonCharacter service
Next, we will create a cartoon-character service that can be used from a multitude of components. To this end, execute the following instruction from a terminal window while in the root folder:
ng generate service CartoonCharacter
This produces the following files:
src\app\cartoon-character.service.spec.ts
src\app\cartoon-character.service.ts
Add an empty
getCartoonCharacters() method to the
CartoonCharacterService class as follows:
getCartoonCharacters(): void { }
Add the following import commands to the new service
cartoon-character.service.ts file:
import {DUMMY_DATA} from './data/dummy-data';
import {CartoonCharacter} from './cartoon-character'
In
app.component.ts, change ‘
characters = DUMMY_DATA;’ to ‘
characters: CartoonCharacter[];’ and delete the DUMMY_DATA import statement. The
app.component.ts should look like this:
import { Component } from '@angular/core';
import {CartoonCharacter} from './cartoon-character';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
/*
character: CartoonCharacter = {
PersonId: 1,
FirstName: "Fred",
LastName: "Flintstone",
Occupation: "Mining Manager",
Gender: "M",
Picture: "http://flintstones.zift.ca/images/flintstone/fred.png",
};
*/
selected: CartoonCharacter;
characters: CartoonCharacter[];
onSelect(character: CartoonCharacter): void {
this.selected = character;
}
}
In the
CartoonCharacterService class, replace the
getCartoonCharacters() method with the following code:
getCartoonCharacters(): CartoonCharacter[] {
return DUMMY_DATA;
}
The
cartoon-character.service.ts file now should look like this:
import { Injectable } from '@angular/core';
import {DUMMY_DATA} from './data/dummy-data';
import {CartoonCharacter} from './cartoon-character'
@Injectable()
export class CartoonCharacterService {
constructor() { }
getCartoonCharacters(): CartoonCharacter[] {
return DUMMY_DATA;
}
}
Using the CartoonCharacter service
We will be using the new service in
app.component.ts. Back in that file, import the service with the following statement:
import {CartoonCharacterService} from './cartoon-character.service';
We do not want to instantiate a new instance of
CartoonCharacterService. Instead, we will use dependency injection. Add the following constructor to the
AppComponent class:
constructor(private cartoonService: CartoonCharacterService) { }
Add the following providers array to
@Component for the
AppComponent class:
providers: [CartoonCharacterService]
Add the following method to
AppComponent:
getCartoonCharacters(): void {
this.characters = this.cartoonService.getCartoonCharacters();
}
Instead of making a call to
getCartoonCharacters() from within the constructor of
AppComponent, we will make a call when the component gets initialized. This is best done in the
ngOnInit() method.
To do this we need to import
OnInit and implement
OnInit. We will then make a call to
getCartoonCharacters() from within the
ngOnInit() method. Here’s what
app.component.ts should now look like:
import { Component, OnInit } from '@angular/core';
import {CartoonCharacter} from './cartoon-character';
import {CartoonCharacterService} from './cartoon-character.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [CartoonCharacterService]
})
export class AppComponent implements OnInit {
selected: CartoonCharacter;
characters: CartoonCharacter[];
constructor(private cartoonService: CartoonCharacterService) { }
onSelect(character: CartoonCharacter): void {
this.selected = character;
}
getCartoonCharacters(): void {
this.characters = this.cartoonService.getCartoonCharacters();
}
ngOnInit(): void {
this.getCartoonCharacters();
}
}
Our application should be working fine now.
There is one problem, though. Our service is not making async calls. We will change that. Change the
getCartoonCharacters() method in the
CartoonCharacterService class to the following:
getCartoonCharacters(): Promise<CartoonCharacter[]> {
return Promise.resolve(DUMMY_DATA);
}
Since this returns a promise, we will need to change the way this method is called. Back in
AppComponent class, change the
getCartoonCharacters() method to the following:
getCartoonCharacters(): void {
this.cartoonService.getCartoonCharacters()
.then(characters => this.characters = characters);
}
Routing around our app
We will build a menu system with links allowing us to choose what it is we want to do. Now, we only have one page. This is not very realistic for a larger project. Our revised app should display a shell with a choice of views (Dashboard and Cartoon Characters) and then default to one of them. The first task is to move the display of cartoon-characters out of
AppComponent and into its own
CartoonCharacterComponent.
Let us create a new component named
CartoonCharacterComponent with the following terminal command:
ng generate component CartoonCharacter
The following files get generated:
src\app\cartoon-character\cartoon-character.component.css
src\app\cartoon-character\cartoon-character.component.html
src\app\cartoon-character\cartoon-character.component.spec.ts
src\app\cartoon-character\cartoon-character.component.ts
Since
AppComponent is doing the work that we want
CartoonCharacterComponent to do, let us simply copy the contents of
AppComponent to
CartoonCharacterComponent:
|
Copy Contents from … | to … |
app.component.html | cartoon-character.html |
app.component.css | cartoon-character.css |
app.component.ts | cartoon-character.ts |
Also:
- change the app-root selector in copied contents of cartoon-character.component.ts to cartoon-character-component
- delete the providers array line – providers: [CartoonCharacterService]
- adjust the filenames in cartoon-character.component.ts to match the actual .html and .css files
- app.component.html should contain only the following markup:
<cartoon-character-component></cartoon-character-component>
Make sure the content of
app.component.ts has the following code’:
import { Component } from '@angular/core';
import { CartoonCharacterService } from './cartoon-character.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [CartoonCharacterService]
})
export class AppComponent {
title = 'The Flintstones';
}
Check out the application, it should be working fine just like before.
Routing
The next big task is to add routes and links to our main landing page. Open
app.module.ts in your editor and add the following import statement:
import { RouterModule } from '@angular/router';
Define our first route by adding the following to the
imports array:
RouterModule.forRoot([
{
path: 'characters',
component: CartoonCharacterComponent
}
])
app.module.ts now looks like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { CharacterDetailComponent } from './character-detail/character-detail.component';
import { CartoonCharacterComponent } from './cartoon-character/cartoon-character.component';
import { RouterModule } from '@angular/router';
@NgModule({
declarations: [
AppComponent,
CharacterDetailComponent,
CartoonCharacterComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot([
{
path: 'characters',
component: CartoonCharacterComponent
}
])
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }
The route definition has the following parts:
- path: the router matches this route's path to the URL in the browser address bar (characters).
- component: the component that the router should create when navigating to this route (CartoonCharacterComponent).
Router Outlet
If we paste the path,
/characters, into the browser address bar, the router should match it to the
characters route and display the
CartoonCharacterComponent. But where?
We must tell it where by adding a
<router-outlet> element to the bottom of the template. The router displays each component immediately below the
<router-outlet> as we navigate through the application.
Edit the
app.component.html file. Replace
<cartoon-characters></cartoon-characters> with the following markup:
<nav>
<a routerLink="/characters">Cartoon Characters</a>
</nav>
<router-outlet></router-outlet>
When you run the application, you will see a link as follows:
When you click on the “
Cartoon Characters” link, two things happen:
- The address line in your browser changes to: http://localhost:4200/characters
- The contents of CartoonCharacterComponent are injected into <router-outlet></router-outlet>
Adding another dashboard link
Create another component named
DashboardComponent by executing the following in a terminal window:
ng generate component Dashboard
This causes the following files to be created for us:
src\app\dashboard\dashboard.component.css
src\app\dashboard\dashboard.component.html
src\app\dashboard\dashboard.component.spec.ts
src\app\dashboard\dashboard.component.ts
Add another route to app.module.ts as shown below:
{
path: 'dashboard',
component: DashboardComponent
},
Next, let us add one more link to
app.component.html. Our latest iteration of
app.component.html looks like this:
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/characters">Cartoon Characters</a>
</nav>
<router-outlet></router-outlet>
At this point, when you run the application this is what you should see:
Of course, we still need to develop our
DashboardComponent class
. Modify
dashboard.component.ts so that it looks like this:
import { Component, OnInit } from '@angular/core';
import {CartoonCharacter} from '../cartoon-character';
import {CartoonCharacterService} from '../cartoon-character.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
characters: CartoonCharacter[];
constructor(private cartoonService: CartoonCharacterService) { }
ngOnInit() {
this.cartoonService.getCartoonCharacters()
.then(results => this.characters = results.slice(0, 4));
}
}
Also, modify
dashboard.component.html so that it looks like this:
<h3>The Flintstones Family</h3>
<p *ngFor="let c of characters" class="col-lg-4">
{{c.FirstName}} {{c.LastName}}
</p>
Refresh the browser and see four cartoon characters in the new dashboard as shown below:
Click on “
Cartoon Characters” and you will see our table with details.
Routing to our Cartoon Character details
One refinement that is overdue is to navigate to a details page if the user clicks on a character on our dashboard.
We need to add an additional method to
CartoonCharacterService that retrieves a single record by id. Open
CartoonCharacterService and add a
getCartoonCharacterById() method that filters the cartoon-characters list from
getCartoonCharacters() by
id:
getCartoonCharacterById(id: number): Promise<CartoonCharacter> {
return this.getCartoonCharacters()
.then(result => result.find(character => character.PersonId === id));
}
We'll add a route to the
CharacterDetailComponent in
app.module.ts where our other routes are configured. Add the following route definition to app.module.ts
:
{
path: 'detail/:id',
component: CharacterDetailComponent
},
The colon (:) in the path indicates that
:id is a placeholder to be filled with a specific cartoon character
id when navigating to the
CharacterDetailComponent.
The revised
CharacterDetailComponent should take the
id parameter from the
params observable in the
ActivatedRoute service and use the
CartoonCharacterService to fetch the cartoon character with that
id.
Add the following import statements to
character-detail.component.ts:
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { CartoonCharacterService } from '../cartoon-character.service';
Let's have the
ActivatedRoute,
CartoonCharacterService and
Location services injected into the constructor, saving their values in private fields:
constructor(
private cartoonService: CartoonCharacterService,
private route: ActivatedRoute,
private location: Location
) { }
Inside the
ngOnInit lifecycle method, we use the
params observable to extract the
id parameter value from the
ActivatedRoute service and use the
CartoonCharacterService to fetch the cartoon-character with that
id.
ngOnInit() {
this.route.params.forEach((params: Params) => {
let id = +params['id'];
this.cartoonService.getCartoonCharacterById(id)
.then(result => this.character = result);
});
}
Notice how we extract the id by calling the
forEach method, which will deliver our array of route parameters. The cartoon character
id is a number. Route parameters are always strings. So, we convert the route parameter value to a number with the JavaScript (+) operator.
Also, add this method to
character-detail.component.ts so that we can go back:
goBack(): void {
this.location.back();
}
Add the following markup to character-detail.component.html so that it now looks like this:
<div *ngIf="character">
<p>Person ID: {{character.PersonId}}</p>
<p>First Name: {{character.FirstName}}</p>
<p>Last Name: {{character.LastName}}</p>
<p>Occupation: {{character.Occupation}}</p>
<p>Gender: {{character.Gender}}</p>
<p>Picture: <img src="{{character.Picture}}" alt="{{character.FirstName}} {{character.LastName}}" /></p>
<div>
<p><label>First Name: </label><input [(ngModel)]="character.FirstName" placeholder="FirstName"></p>
<p><label>Last Name: </label><input [(ngModel)]="character.LastName" placeholder="LastName"></p>
<p><label>Occupation: </label><input [(ngModel)]="character.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="character.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="character.Picture" placeholder="Picture"></p>
</div>
<button (click)="goBack()">Back</button>
</div>
When a user selects a cartoon-character on the dashboard, the app should navigate to the
CharacterDetailComponent to view and edit the selected cartoon-character.
To achieve this effect, reopen the
dashboard.component.html and replace the repeated
<div *ngFor...> tags with
<a> tags so that it looks like this:
<h3>The Flintstones Family</h3>
<a *ngFor="let c of characters" [routerLink]="['/detail', c.PersonId]" class="col-lg-4">
<p>{{c.FirstName}} {{c.LastName}}</p>
</a>
Refresh the browser and select a cartoon-character from the dashboard; the app should navigate directly to that cartoon-character’s details.
Clicking on the “Back” button will take you back in the history of the browser.
Refactor routes to a Routing Module
It is wise to put all of our routing rules in a separate file than
app.module.ts so it does not get overly bloated. This is especially true if our application is large and has many routes. To this end, create an
app-routing.module.ts file in the same folder as
app.module.ts. Give it the following contents extracted from the
AppModule class:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { CartoonCharacterComponent } from './cartoon-character/cartoon-character.component';
import { CharacterDetailComponent } from './character-detail/character-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: CharacterDetailComponent },
{ path: 'characters', component: CartoonCharacterComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
Next, let’s update
app.module.ts so that it uses
app-routing.module.ts. Add the following import statement to
app.module.ts.
import { AppRoutingModule } from './app-routing.module';
Also, change the imports array so it looks like this:
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
This would be the latest state of
app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { CharacterDetailComponent } from './character-detail/character-detail.component';
import { CartoonCharacterComponent } from './cartoon-character/cartoon-character.component';
import { RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [
AppComponent,
CharacterDetailComponent,
CartoonCharacterComponent,
DashboardComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule { }
Our application functions properly and behaves as expected.
Select a Cartoon-Character in the CartoonCharacterComponent
Comment out the last line in cartoon-character.component.html so that it looks like this:
<!--
<app-character-detail [character]="selected"></app-character-detail>
-->
When the user selects a cartoon-character from the list, we don't go to the detail page. We show a mini-detail on this page instead and make the user click a button to navigate to the full detail page.
Add the following HTML fragment at the bottom of
cartoon-character.component.html:
<div *ngIf="selected">
<h2>
{{selected.FirstName | uppercase}} {{selected.LastName | uppercase}} is favorite cartoon character.
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
Update the Update the CartoonCharacterComponent class as follows:
1. Import Router from the Angular router library with the following import statement:
import { Router } from '@angular/router';
2. Inject the Router in the constructor (along with the CartoonCharacterService) as follow:
constructor(
private cartoonService: CartoonCharacterService,
private router: Router
) { }
3. Implement gotoDetail() by calling the router.navigate method as shown below:
gotoDetail(): void {
this.router.navigate(['/detail', this.selected.PersonId]);
}
Refresh your browser and checkout the new behavior of the application.
Styling:
Replace
dashboard.component.css with the following:
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
Replace contents of
cartoon-character.component.css with the following:
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
Replace contents of
app.component.css with the following:
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
h2 {
font-size: 2em;
margin-top: 0;
padding-top: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
}
To set the global application styles, replace contents of
styles.css with:
/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
/* . . . */
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}
Navigate through the application and experience the refined look.
Working with real data
The data we are viewing is being served from a static array. We need to interact with a real remote service. We will work with a service located at
http://flintstones.zift.ca/flintstones/ that delivers the same data as the static array used previously.
Let's convert
getCartoonCharacters () in
cartoon-character.ts to use HTTP. Add the following import statements to
cartoon-character.service.ts:
import { Headers, Http, Response } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
Inject the http service into the service’s constructor with the following code:
constructor(private http: Http) { }
Add the following instance variable to the
CartoonCharacterService class:
private BASE_URL = "http://flintstones.zift.ca/api/flintstones";
Change method
getCartoonCharacters() in
cartoon-character.service.ts to the following code:
getCartoonCharacters(): Promise<CartoonCharacter[]> {
return this.http.get(this.BASE_URL)
.toPromise()
.then(response => response.json() as CartoonCharacter[])
.catch(this.handleError);
}
The catch block calls a
handleError() method. Add this
handleError() method to the
CartoonCharacterService class:
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
Test the application and you will determine that it works just as before. The big difference is that it is indeed asynchronously retrieving data from a remote data source.
Updating data
The next challenge is to update data. Add the following code to the
CartoonCharacterService class:
private headers = new Headers({'Content-Type': 'application/json'});
update(character: CartoonCharacter): Promise<CartoonCharacter> {
const url = `${this.BASE_URL}/${character.PersonId}`;
return this.http
.put(url, JSON.stringify(character), {headers: this.headers})
.toPromise()
.then(() => character)
.catch(this.handleError);
}
Add the following
save button to the bottom of the
character-detail.component.html file right after the
back button:
<button (click)="save()">Save</button>
The
save method persists cartoon-character data changes using the service’s
update method, then navigates back to the previous view. Add this
save() method to
character-detail.component.ts:
save(): void {
this.cartoonService.update(this.character)
.then(() => this.goBack());
}
Refresh the browser and give it a try. Changes to cartoon-character data should now persist.
Add data
Add the following method to the
CartoonCharacterService class:
create(newCartoonCharacter: CartoonCharacter): Promise<CartoonCharacter> {
return this.http
.post(this.BASE_URL, JSON.stringify(newCartoonCharacter), {headers: this.headers})
.toPromise()
.then(res => res.json().data)
.catch(this.handleError);
}
Add this markup to
cartoon-character.component.html:
<div>
<p><label>First Name: </label><input [(ngModel)]="newCharacter.FirstName" placeholder="First Name"></p>
<p><label>Last Name: </label><input [(ngModel)]="newCharacter.LastName" placeholder="Last Name"></p>
<p><label>Occupation: </label><input [(ngModel)]="newCharacter.Occupation" placeholder="Occupation"></p>
<p><label>Gender: </label><input [(ngModel)]="newCharacter.Gender" placeholder="M or F"></p>
<p><label>Picture: </label><input [(ngModel)]="newCharacter.Picture" placeholder="Picture URL"></p>
<button (click)="add(newCharacter);">
Add
</button>
</div>
Add the following to the
CartoonCharacterComponent class:
newCharacter: CartoonCharacter = new CartoonCharacter();
add(newCartoonCharacter: CartoonCharacter): void {
newCartoonCharacter.FirstName = newCartoonCharacter.FirstName.trim();
newCartoonCharacter.LastName = newCartoonCharacter.LastName.trim();
newCartoonCharacter.Occupation = newCartoonCharacter.Occupation.trim();
newCartoonCharacter.Gender = newCartoonCharacter.Gender.trim();
newCartoonCharacter.Picture = newCartoonCharacter.Picture.trim();
if (!newCartoonCharacter) { return; }
this.cartoonService.create(newCartoonCharacter)
.then(newCartoonCharacter => {
this.selected = null;
this.router.navigate(['./dashboard']);
});
}
Refresh the browser and add some sample cartoon-character data.
Deleting data
Add a new column to the table in
cartoon-character.component.html and put the following button in a cell after
{{c.Gender}}:
<button class="delete" (click)="delete(c); $event.stopPropagation()">x</button>
The entire table in
cartoon-character.component.html will look like this:
<table *ngIf="characters" border="1">
<thead>
<tr>
<th>Person ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Occupation</th>
<th>Gender</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of characters; let i=index;" (click)="onSelect(c)" [class.selected]="c === selected">
<td>{{c.PersonId}}</td>
<td>{{c.FirstName}}</td>
<td>{{c.LastName}}</td>
<td>{{c.Occupation}}</td>
<td>{{c.Gender}}</td>
<td><button class="delete" (click)="delete(c); $event.stopPropagation()">x</button></td>
</tr>
</tbody>
</table>
Add the following
delete() method to the
CartoonCharacterService class:
delete(id: number): Promise<void> {
const url = `${this.BASE_URL}/${id}`;
return this.http.delete(url, {headers: this.headers})
.toPromise()
.then(() => null)
.catch(this.handleError);
}
Add this
delete() method to the
CartoonCharacterComponent class:
delete(delCharacter: CartoonCharacter): void {
this.cartoonService
.delete(delCharacter.PersonId)
.then(() => {
this.characters = this.characters.filter(c => c !== delCharacter);
if (this.selected === delCharacter) { this.selected = null; }
});
}
Refresh the browser and try the new delete the records that you created.
Conclusion
In this post, we have developed a well structured Angular 2 application that interacts with real data. If you and retrieve, add, update, and delete data then you are in a position to do some true an real world single page applications with Angular 2.
References:
https://angular.io/docs/ts/latest/tutorial/