Display a Heroes List
In this page, you'll expand the Tour of Heroes app to display a list of heroes, and allow users to select a hero and display the hero's details.
Create mock heroes
You'll need some heroes to display.
Eventually you'll get them from a remote data server. For now, you'll create some mock heroes and pretend they came from the server.
Create a file called mock-heroes.ts
in the src/app/
folder. Define a HEROES
constant as an array of ten heroes and export it. The file should look like this.
import { Hero } from './hero'; export const HEROES: Hero[] = [ { id: 11, name: 'Mr. Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ];
Displaying heroes
You're about to display the list of heroes at the top of the HeroesComponent
.
Open the HeroesComponent
class file and import the mock HEROES
.
import { HEROES } from '../mock-heroes';
In the same file (HeroesComponent
class), define a component property called heroes
to expose HEROES
array for binding.
export class HeroesComponent implements OnInit { heroes = HEROES;
List heroes with *ngFor
Open the HeroesComponent
template file and make the following changes:
- Add an
<h2>
at the top, - Below it add an HTML unordered list (
<ul>
) - Insert an
<li>
within the<ul>
that displays properties of ahero
. - Sprinkle some CSS classes for styling (you'll add the CSS styles shortly).
Make it look like this:
<h2>My Heroes</h2> <ul class="heroes"> <li> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul>
Now change the <li>
to this:
<li *ngFor="let hero of heroes">
The *ngFor
is Angular's repeater directive. It repeats the host element for each element in a list.
In this example
-
<li>
is the host element -
heroes
is the list from theHeroesComponent
class. -
hero
holds the current hero object for each iteration through the list.
Don't forget the asterisk (*) in front of
ngFor
. It's a critical part of the syntax.
After the browser refreshes, the list of heroes appears.
Style the heroes
The heroes list should be attractive and should respond visually when users hover over and select a hero from the list.
In the first tutorial, you set the basic styles for the entire application in styles.css
. That stylesheet didn't include styles for this list of heroes.
You could add more styles to styles.css
and keep growing that stylesheet as you add components.
You may prefer instead to define private styles for a specific component and keep everything a component needs— the code, the HTML, and the CSS —together in one place.
This approach makes it easier to re-use the component somewhere else and deliver the component's intended appearance even if the global styles are different.
You define private styles either inline in the @Component.styles
array or as stylesheet file(s) identified in the @Component.styleUrls
array.
When the CLI generated the HeroesComponent
, it created an empty heroes.component.css
stylesheet for the HeroesComponent
and pointed to it in @Component.styleUrls
like this.
@Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] })
Open the heroes.component.css
file and paste in the private CSS styles for the HeroesComponent
. You'll find them in the final code review at the bottom of this guide.
Styles and stylesheets identified in
@Component
metadata are scoped to that specific component. Theheroes.component.css
styles apply only to theHeroesComponent
and don't affect the outer HTML or the HTML in any other component.
Master/Detail
When the user clicks a hero in the master list, the component should display the selected hero's details at the bottom of the page.
In this section, you'll listen for the hero item click event and update the hero detail.
Add a click event binding
Add a click event binding to the <li>
like this:
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
This is an example of Angular's event binding syntax.
The parentheses around click
tell Angular to listen for the <li>
element's click
event. When the user clicks in the <li>
, Angular executes the onSelect(hero)
expression.
onSelect()
is a HeroesComponent
method that you're about to write. Angular calls it with the hero
object displayed in the clicked <li>
, the same hero
defined previously in the *ngFor
expression.
Add the click event handler
Rename the component's hero
property to selectedHero
but don't assign it. There is no selected hero when the application starts.
Add the following onSelect()
method, which assigns the clicked hero from the template to the component's selectedHero
.
selectedHero: Hero; onSelect(hero: Hero): void { this.selectedHero = hero; }
Update the details template
The template still refers to the component's old hero
property which no longer exists. Rename hero
to selectedHero
.
<h2>{{selectedHero.name | uppercase}} Details</h2> <div><span>id: </span>{{selectedHero.id}}</div> <div> <label>name: <input [(ngModel)]="selectedHero.name" placeholder="name"> </label> </div>
Hide empty details with *ngIf
After the browser refreshes, the application is broken.
Open the browser developer tools and look in the console for an error message like this:
HeroesComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined
Now click one of the list items. The app seems to be working again. The heroes appear in a list and details about the clicked hero appear at the bottom of the page.
What happened?
When the app starts, the selectedHero
is undefined
by design.
Binding expressions in the template that refer to properties of selectedHero
— expressions like {{selectedHero.name}}
— must fail because there is no selected hero.
The fix
The component should only display the selected hero details if the selectedHero
exists.
Wrap the hero detail HTML in a <div>
. Add Angular's *ngIf
directive to the <div>
and set it to selectedHero
.
Don't forget the asterisk (*) in front of
ngIf
. It's a critical part of the syntax.
<div *ngIf="selectedHero"> <h2>{{selectedHero.name | uppercase}} Details</h2> <div><span>id: </span>{{selectedHero.id}}</div> <div> <label>name: <input [(ngModel)]="selectedHero.name" placeholder="name"> </label> </div> </div>
After the browser refreshes, the list of names reappears. The details area is blank. Click a hero and its details appear.
Why it works
When selectedHero
is undefined, the ngIf
removes the hero detail from the DOM. There are no selectedHero
bindings to worry about.
When the user picks a hero, selectedHero
has a value and ngIf
puts the hero detail into the DOM.
Style the selected hero
It's difficult to identify the selected hero in the list when all <li>
elements look alike.
If the user clicks "Magneta", that hero should render with a distinctive but subtle background color like this:
That selected hero coloring is the work of the .selected
CSS class in the styles you added earlier. You just have to apply the .selected
class to the <li>
when the user clicks it.
The Angular class binding makes it easy to add and remove a CSS class conditionally. Just add [class.some-css-class]="some-condition"
to the element you want to style.
Add the following [class.selected]
binding to the <li>
in the HeroesComponent
template:
[class.selected]="hero === selectedHero"
When the current row hero is the same as the selectedHero
, Angular adds the selected
CSS class. When the two heroes are different, Angular removes the class.
The finished <li>
looks like this:
<li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li>
Final code review
Your app should look like this live example.
Here are the code files discussed on this page, including the HeroesComponent
styles.
import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HEROES } from '../mock-heroes'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { heroes = HEROES; selectedHero: Hero; constructor() { } ngOnInit() { } onSelect(hero: Hero): void { this.selectedHero = hero; } }
<h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <div *ngIf="selectedHero"> <h2>{{selectedHero.name | uppercase}} Details</h2> <div><span>id: </span>{{selectedHero.id}}</div> <div> <label>name: <input [(ngModel)]="selectedHero.name" placeholder="name"> </label> </div> </div>
/* HeroesComponent's private CSS styles */ .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .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; margin-right: .8em; border-radius: 4px 0 0 4px; }
Summary
- The Tour of Heroes app displays a list of heroes in a Master/Detail view.
- The user can select a hero and see that hero's details.
- You used
*ngFor
to display a list. - You used
*ngIf
to conditionally include or exclude a block of HTML. - You can toggle a CSS style class with a
class
binding.
© 2010–2019 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v6.angular.io/tutorial/toh-pt2