Tutorial: Display a Heroes List

REFERENCES
[1]: https://angular.io/tutorial/toh-pt2 "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.

src/app/mock-heroes.ts
import { Hero } from './hero';

export const HEROES: Hero[] = [
  {
    id: 1,
    name: 'Captain America',
  },
  {
    id: 2,
    name: 'Iron Man',
  },
  {
    id: 3,
    name: 'Thor',
  },
  {
    id: 4,
    name: 'Hulk',
  },
  {
    id: 5,
    name: 'Black Widow',
  },
  {
    id: 6,
    name: 'Ant-Man',
  },
  {
    id: 7,
    name: 'Hawkeye',
  },
  {
    id: 8,
    name: 'Doctor Strange',
  },
  {
    id: 9,
    name: 'Black Panther',
  },
  {
    id: 10,
    name: 'Spider-Man',
  }
];

Please note that in my own example, I included other attributes for each hero including powers, abilities, and realName. These are all as strings with powers and abilities as optional.

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.

Add a heroes property to the class that exposes these heroes for binding.

src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
// import { Hero } from '../hero';  <-- Unnecessary anymore
import { HEROES } from '../mock-heroes';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})

export class HeroesComponent implements OnInit {
  // hero: Hero = {
  //   id: 1,
  //   name: 'Captain America',
  // };
  heroes = HEROES;

  constructor() { }

  ngOnInit() { }
}

Introduction the <mat-list> component

REFERENCE
[2]: https://material.angular.io/components/list/overview "List"

<mat-list> is a container component that wraps and formats a series of line items. As the base list component, it provides Material Design styling, but no behavior of its own.

An <mat-list> element contains a number of <mat-list-item> elements.

<mat-list>
 <mat-list-item> Pepper </mat-list-item>
 <mat-list-item> Salt </mat-list-item>
 <mat-list-item> Paprika </mat-list-item>
</mat-list>

Use mat-nav-list tags for navigation lists (i.e. lists that have anchor tags).

Simple navigation lists can use the mat-list-item attribute on anchor tag elements directly:

<mat-nav-list>
   <a mat-list-item href="..." *ngFor="let link of links"> {{ link }} </a>
</mat-nav-list>

For more complex navigation lists (e.g. with more than one target per item), wrap the anchor element in an <mat-list-item>.

<mat-nav-list>
  <mat-list-item *ngFor="let link of links">
     <a matLine href="...">{{ link }}</a>
     <button mat-icon-button (click)="showInfo(link)">
        <mat-icon>info</mat-icon>
     </button>
  </mat-list-item>
</mat-nav-list>
Multi-line lists

For lists that require multiple lines per item, annotate each line with an matLine attribute. Whichever heading tag is appropriate for your DOM hierarchy should be used (not necessarily <h3> as shown in the example).

Lists with icons

To add an icon to your list item, use the matListIcon attribute.

Lists with avatars

To include an avatar image, add an image tag with an matListAvatar attribute.

Dense lists

Lists are also available in "dense layout" mode, which shrinks the font size and height of the list to suit UIs that may need to display more information. To enable this mode, add a dense attribute to the main mat-list tag.

List heroes with *ngFor

Open the HeroesComponent template file and make the following changes:

Make it look like this:

src/app/heroes/heroes.component.html
<div class="row mt-3">
  <div class="col-sm-6 offset-sm-3">
    <mat-card class="mat-elevation-z3 mat-typography">
      <mat-nav-list class="">
        <mat-list-item *ngFor="let hero of heroes" (click)="onSelect(hero)"
                       [class.selected]="hero === selectedHero">
          <a matLine href="#" aria-disabled="false">
            <img matListAvatar src="{{ hero.avatarPath }}" alt="">
            <p class="hero-list blue-grey-text text-lighten-3">{{ hero.id }}</p>
            <p class="hero-list">{{ hero.name}}</p>
          </a>
        </mat-list-item>
      </mat-nav-list>
    </mat-card>
  </div>
</div>

The *ngFor is Angular's repeater directive. It repeats the host element for each element in a list.

In this example:

  • <mat-list-item> is the host element.
  • heroes is the list from the HeroesComponent 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.

src/app/heroes/heroes.component.ts
...

@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.

Styles and stylesheets identified in @Component metadata are scoped to that specific component. The heroes.component.css styles apply only to the HeroesComponent and don't affect the outer HTML or the HTML in any other component.

src/app/heroes/heroes.component.css
.hero-list {
  display: inline;
}

/* Default avatar size is 40x40 */
.mat-list-avatar {
  width: 30px !important;
  height: 30px !important;
}

/* Default height is 56px */
.mat-list-item {
  height: 48px !important;
}

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 <mat-list-item> like this:

src/app/heroes/heroes.component.html
...

<mat-list-item *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 <mat-list-item>/<li> element's click event. When the user clicks in the <mat-list-item>, 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 <mat-list-item>, 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.

src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HEROES } from '../mock-heroes';

@Component({
  ...
})

export class HeroesComponent implements OnInit {
  heroes = HEROES;
  selectedHero: Hero;

  constructor() { }

  ngOnInit() { }

  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.

...

<div class="row">
  <div class="col-sm-6 offset-sm-3">
    <mat-card class="mat-elevation-z4">
      <mat-card-header>
        <img mat-card-avatar src="{{ selectedHero.avatarPath }}" alt="{{ selectedHero.name }} image">
        <mat-card-title>{{ selectedHero.name | uppercase }}</mat-card-title>
        <mat-card-subtitle><span>ID:</span> {{ selectedHero.id }}</mat-card-subtitle>
      </mat-card-header>
      <img mat-card-image src="{{ selectedHero.imgPath }}" alt="{{ selectedHero.name }} image">
      <mat-card-content>
        <mat-input-container class="form-container">
          <input type="text" placeholder="Name:" matInput [(ngModel)]="selectedHero.name">
        </mat-input-container>
        <p><b>Real Name:</b> {{ selectedHero.realName }}</p>
        <p><b>Powers:</b> {{ selectedHero.powers }}</p>
        <p><b>Abilities:</b> {{ selectedHero.abilities }}</p>
      </mat-card-content>
      <mat-card-actions>
        <button mat-button>LIKE</button>
      </mat-card-actions>
    </mat-card>
  </div>
</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.

src/app/heroes/heroes.component.html
...

<div class="row" *ngIf="selectedHero">
  ...
</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.

Introducing the mat-button attribute

Angular Material buttons are native <button> or <a> elements enhanced with Material Design styling and ink ripples.

Native <button> and <a> elements are always used in order to provide the most straightforward and accessible experience for users. A <button> element should be used whenever some action is performed. An <a> element should be used whenever the user will navigate to another view.

There are five button variants, each applied as an attribute:

Attribute Description
mat-button Rectangular button w/ no elevation.
mat-raised-button Rectangular button w/ elevation
mat-icon-button Circular button with a transparent background, meant to contain an icon
mat-fab Circular button w/ elevation, defaults to theme's accent color
mat-mini-fab Same as mat-fab but smaller

Style the selected hero

It's difficult to identify the selected hero in the list when all <mat-list-item> elements look alike.

If the user clicks "Iron Man", that hero should render with a distinctive but subtle background color like this:

Hero select styling

First, change your <mat-list-item> to use the mat-button attribute so there is an overlay on hover/mouse over. Also add a <mat-card> component so the whole list looks like it's sitting on a card.

src/app/heroes/heroes.component.html
<div class="row">
  <div class="col-sm-6 offset-sm-3">
    <mat-card class="mat-elevation-z3">
      <mat-list class="align-items-center">
        <mat-list-item *ngFor="let hero of heroes" (click)="onSelect(hero)">
            <a href="#" mat-button class="mat-button" aria-disabled="false" style="padding: 8px">
            <img matListAvatar src="{{ hero.avatarPath }}" alt="">
            <p class="hero-list blue-grey-text text-lighten-3">{{ hero.id }}</p>
            <p class="hero-list">{{ hero.name}}</p>
          </a>
          <div class="mat-button-ripple mat-ripple" mat-ripple></div>
          <div class="mat-button-focus-overlay"></div>
        </mat-list-item>
      </mat-list>
    </mat-card>
  </div>
</div>

Now add a css class style to color the selected <mat-list-item>.

src/app/heroes/heroes.component.css
...

.selected {
  background-color: #607d8b !important;
  color: white;
}

...

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 <mat-list-item> in the HeroesComponent template.

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:

src/app/heroes/heroes.component.html
<div class="row">
  <div class="col-sm-6 offset-sm-3">
    <mat-card class="mat-elevation-z3">
      <mat-list class="valign-wrapper">
        <mat-list-item *ngFor="let hero of heroes" 
                       (click)="onSelect(hero)" 
                       [class.selected]="hero === selectedHero">

          ...

        </mat-list-item>
      </mat-list>
    </mat-card>
  </div>
</div>

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.

results matching ""

    No results matching ""