The Tour of Heroes tutorial covers the fundamentals of Angular.
In this tutorial you will build an app that helps a staffing agency manage its stable of heroes.
This basic app has many of the features you'd expect to find in a data-driven application. It acquires and displays a list of heroes, edits a selected hero's detail, and navigates among different views of heroic data.
You'll learn enough Angular to get started and gain confidence that Angular can do whatever you need it to do.
Install the Angular CLI, if you haven't already done so.
npm install -g @angular/cli
git clone https://github.com/jcdesousa/angular-toh.git angular-toh
Go to the project directory and install the dependencies.
cd angular-toh
npm install
The ng serve
command builds the app, starts the development server, watches the source files, and rebuilds the app as you make changes to those files.
The --open
flag opens a browser to http://localhost:4200/
.
ng serve --open
You should see the app running in your browser.
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module
.
Run ng build
to build the project. The build artifacts will be stored in the dist/
directory. Use the -prod
flag for a production build.
Run ng test
to execute the unit tests via Karma.
Run ng e2e
to execute the end-to-end tests via Protractor.
Update the binding in the template to announce the hero's name and show both id
and name
in a details layout like this:
<!-- src/app/hero-detail/hero-detail.component.html !-->
<div>
<h2>{{ hero.name }} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>
</div>
A URL like ~/detail/11
would be a good URL for navigating to the Hero Detail view of the hero whose id
is 11
.
Open AppRoutingModule
and import HeroDetailComponent
.
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
Then add a parameterized route to the AppRoutingModule.routes array that matches the path pattern to the hero detail view.
{ path: 'detail/:id', component: HeroDetailComponent },
Modify the selectedHero.name binding like this.
<!-- src/app/heroes/hero-detail.component.html !-->
<h2>{{ hero.name | uppercase }} Details</h2>
Now the selected hero's name is displayed in capital letters.
The component should only display the selected hero details if the hero exists.
<!-- src/app/heroes/hero-detail.component.html (*ngIf) -->
<div *ngIf="hero">
...
</div>
Users should be able to edit the selected hero name in an <input>
textbox.
To automate that data flow, setup a two-way data binding between the <input>
form element and the hero.name
property.
<!-- src/app/heroes/hero-detail.component.html !-->
<input [(ngModel)]="hero.name" placeholder="name">
Although ngModel is a valid Angular directive, it isn't available by default.
It belongs to the optional FormsModule
and you must opt-in to using it.
Open AppModule
(app.module.ts
) and import the FormsModule
symbol from the @angular/forms
library.
//app.module.ts (FormsModule symbol import)
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
Then add FormsModule
to the @NgModule
metadata's imports
array, which contains a list of external modules that the app needs.
//app.module.ts ( @NgModule imports)
imports: [
BrowserModule,
FormsModule
],
By clicking the browser's back button, you can go back to the hero list or dashboard view, depending upon which sent you to the detail view.
It would be nice to have a button on the HeroDetail
view that can do that.
<!-- src/app/hero-detail/hero-detail.component.html (back button)-->
<button (click)="goBack()">go back</button>
Editing a hero's name in the hero detail view.
As you type, the hero name updates the heading at the top of the page. But when you click the "go back button", the changes are lost.
If you want changes to persist, you must write them back to the server.
At the end of the hero detail template, add a save button with a click
event binding that invokes a new component method named save()
.
<!-- src/app/hero-detail/hero-detail.component.html (save) -->
<button (click)="save()">save</button>
Add the following save() method, which persists hero name changes using the hero service updateHero() method and then navigates back to the previous view.
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
Using the Angular CLI, generate a new component named heroes
.
ng generate component heroes
Import the hero
and HeroService
.
// src/app/heroes/heroes.component.ts (import HEROES)
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
Add a heroes property to the class that exposes these heroes for binding.
// src/app/heroes/heroes.component.ts
heroes: Hero[];
Add a private heroService
parameter of type HeroService
to the constructor.
// src/app/heroes/heroes.component.ts
constructor(private heroService: HeroService) { }
Create a function to retrieve the heroes from the service.
// src/app/heroes/heroes.component.ts
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
While you could call getHeroes()
in the constructor, that's not the best practice.
Reserve the constructor for simple initialization such as wiring constructor parameters to properties. The constructor shouldn't do anything.
// src/app/heroes/heroes.component.ts
ngOnInit() {
this.getHeroes();
}
Open the HeroesComponent
template file and add the following to the top of the file:
<!-- heroes.component.html (template excerpt) -->
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
You intend to navigate to the HeroesComponent
when the URL is something like localhost:4200/heroes
.
Import the HeroesComponent
so you can reference it in a Route
. Then define an array of routes with a single route to that component.
// src/app/app-routing.module.ts
import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
// ...
{ path: 'heroes', component: HeroesComponent }
];
Once you've finished setting up, the router will match that URL to path: 'heroes' and display the HeroesComponent.
<!-- src/app/app-component.html -->
<a routerLink="/heroes">Heroes</a>
The heroes list should be attractive and should respond visually when users hover over and select a hero from the list.
/* src/app/heroes/heroes.component.css (HeroesComponent's private CSS styles) */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroes a:hover {
color:#607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
.button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 194px;
top: -32px;
background-color: gray !important;
color: white;
}
Wrap the badge and name in an anchor element (<a>
), and add a routerLink attribute to the anchor that is the same as in the dashboard template
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
</li>
</ul>
To add a hero, this app only needs the hero's name. You can use an input
element paired with an add button.
Insert the following into the HeroesComponent
template, just after
the heading:
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
Add the add
and delete()
handler to the component.
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
Each hero in the heroes list should have a delete button.
Add the following button element to the HeroesComponent
template, after the hero name in the repeated <li>
element.
<button class="delete" title="delete hero" (click)="delete(hero)">x</button>
The HTML for the list of heroes should look like this:
<!-- src/app/heroes/heroes.component.html (list of heroes) -->
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero" (click)="delete(hero)">x</button>
</li>
</ul>
In this section you will
- add a
MessagesComponent
that displays app messages at the bottom of the screen. - create an injectable, app-wide
MessageService
for sending messages to be displayed - inject
MessageService
into theHeroService
- display a message when
HeroService
fetches heroes successfully.
Use the CLI to create the MessageService
. The --module=app
option tells the CLI to provide this service in the AppModule
,
ng generate service message --module=app
Open MessageService
and replace its contents with the following.
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
The service exposes its cache of messages and two methods: one to add() a message to the cache and another to clear() the cache.
Modify the AppComponent
template to display the MessagesComponent
<!-- /src/app/app.component.html -->
<app-messages></app-messages>
The MessagesComponent
should display all messages, including the message sent by the HeroService
when it fetches heroes.
Open MessagesComponent
and import the MessageService
.
// /src/app/messages/messages.component.ts (import MessageService)
import { MessageService } from '../message.service';
Modify the constructor with a parameter that declares a public messageService
property. Angular will inject the singleton MessageService
into that property when it creates the HeroService
.
// /src/app/messages/messages.component.ts
constructor(public messageService: MessageService) { }
The messageService
property must be public because you're about to bind to it in the template.
Inject MessageService
into the constructor in a private property called messageService
.
// src/app/hero.service.ts
import { MessageService } from './message.service';
constructor(
private http: HttpClient,
private messageService: MessageService) { }
Modify the log
method in HeroService
to send the log message.
// src/app/hero.service.ts
private log(message: string) {
this.messageService.add('HeroService: ' + message);
}
You're at the end of your journey, and you've accomplished a lot. Congrats, You are awesome!