Master/Detail

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.

When you're done with this page, the app should look like this live example.

Where you left off

Before you continue with this page of the Tour of Heroes, verify that you have the following structure after The Hero Editor page. If your structure doesn't match, go back to that page to figure out what you missed.

angular-tour-of-heroes
  src
    app
      app.component.ts
      app.module.ts
    main.ts
    index.html
    styles.css
    systemjs.config.js
    tsconfig.json
  node_modules ...
  package.json

Keep the app transpiling and running

Enter the following command in the terminal window:

npm start

This command runs the TypeScript compiler in "watch mode", recompiling automatically when the code changes. The command simultaneously launches the app in a browser and refreshes the browser when the code changes.

You can keep building the Tour of Heroes without pausing to recompile or refresh the browser.

Displaying heroes

To display a list of heroes, you'll add heroes to the view's template.

Create heroes

Create an array of ten heroes.

src/app/app.component.ts (hero array)
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' }
];

The HEROES array is of type Hero, the class defined in the previous page. Eventually this app will fetch the list of heroes from a web service, but for now you can display mock heroes.

Expose heroes

Create a public property in AppComponent that exposes the heroes for binding.

app.component.ts (hero array property)
heroes = HEROES;

The heroes type isn't defined because TypeScript infers it from the HEROES array.

The hero data is separated from the class implementation because ultimately the hero names will come from a data service.

Display hero names in a template

To display the hero names in an unordered list, insert the following chunk of HTML below the title and above the hero details.

app.component.ts (heroes template)
<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>

Now you can fill the template with hero names.

List heroes with ngFor

The goal is to bind the array of heroes in the component to the template, iterate over them, and display them individually.

Modify the <li> tag by adding the built-in directive *ngFor.

app.component.ts (ngFor)
<li *ngFor="let hero of heroes">

The (*) prefix to ngFor is a critical part of this syntax. It indicates that the <li> element and its children constitute a master template.

The ngFor directive iterates over the component's heroes array and renders an instance of this template for each hero in that array.

The let hero part of the expression identifies hero as the template input variable, which holds the current hero item for each iteration. You can reference this variable within the template to access the current hero's properties.

Read more about ngFor and template input variables in the Showing an array property with *ngFor section of the Displaying Data page and the ngFor section of the Template Syntax page.

Within the <li> tags, add content that uses the hero template variable to display the hero's properties.

app.component.ts (ngFor template)
<li *ngFor="let hero of heroes">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

When the browser refreshes, a list of heroes appears.

Style the heroes

Users should get a visual cue of which hero they are hovering over and which hero is selected.

To add styles to your component, set the styles property on the @Component decorator to the following CSS classes:

src/app/app.component.ts (styles)
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;
  }
`]

Remember to use the backtick notation for multi-line strings.

Adding these styles makes the file much longer. In a later page you'll move the styles to a separate file.

When you assign styles to a component, they are scoped to that specific component. These styles apply only to the AppComponent and don't affect the outer HTML.

The template for displaying heroes should look like this:

src/app/app.component.ts (styled heroes)
<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

Selecting a hero

The app now displays a list of heroes as well as a single hero in the details view. But the list and the details view are not connected. When users select a hero from the list, the selected hero should appear in the details view. This UI pattern is known as "master/detail." In this case, the master is the heroes list and the detail is the selected hero.

Next you'll connect the master to the detail through a selectedHero component property, which is bound to a click event.

Handle click events

Add a click event binding to the <li> like this:

app.component.ts (template excerpt)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
  ...
</li>

The parentheses identify the <li> element's click event as the target. The onSelect(hero) expression calls the AppComponent method, onSelect(), passing the template input variable hero, as an argument. That's the same hero variable you defined previously in the ngFor directive.

Learn more about event binding at the User Input page and the Event binding section of the Template Syntax page.

Add a click handler to expose the selected hero

You no longer need the hero property because you're no longer displaying a single hero; you're displaying a list of heroes. But the user will be able to select one of the heroes by clicking on it. So replace the hero property with this simple selectedHero property:

src/app/app.component.ts (selectedHero)
selectedHero: Hero;

The hero names should all be unselected before the user picks a hero, so you won't initialize the selectedHero as you did with hero.

Add an onSelect() method that sets the selectedHero property to the hero that the user clicks.

src/app/app.component.ts (onSelect)
onSelect(hero: Hero): void {
  this.selectedHero = hero;
}

The template still refers to the old hero property. Bind to the new selectedHero property instead as follows:

app.component.ts (template excerpt)
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>

Hide the empty detail with ngIf

When the app loads, selectedHero is undefined. The selected hero is initialized when the user clicks a hero's name. Angular can't display properties of the undefined selectedHero and throws the following error, visible in the browser's console:

EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]

Although selectedHero.name is displayed in the template, you must keep the hero detail out of the DOM until there is a selected hero.

Wrap the HTML hero detail content of the template with a <div>. Then add the ngIf built-in directive and set it to the selectedHero property of the component.

src/app/app.component.ts (ngIf)
<div *ngIf="selectedHero">
  <h2>{{selectedHero.name}} details!</h2>
  <div><label>id: </label>{{selectedHero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
  </div>
</div>

Don't forget the asterisk (*) in front of ngIf.

The app no longer fails and the list of names displays again in the browser.

When there is no selected hero, the ngIf directive removes the hero detail HTML from the DOM. There are no hero detail elements or bindings to worry about.

When the user picks a hero, selectedHero becomes defined and ngIf puts the hero detail content into the DOM and evaluates the nested bindings.

Read more about ngIf and ngFor in the Structural Directives page and the Built-in directives section of the Template Syntax page.

Style the selected hero

While the selected hero details appear below the list, it's difficult to identify the selected hero within the list itself.

In the styles metadata that you added above, there is a custom CSS class named selected. To make the selected hero more visible, you'll apply this selected class to the <li> when the user clicks on a hero name. For example, when the user clicks "Magneta", it should render with a distinctive but subtle background color like this:

Selected hero

In the template, add the following [class.selected] binding to the <li>:

app.component.ts (setting the CSS class)
[class.selected]="hero === selectedHero"

When the expression (hero === selectedHero) is true, Angular adds the selected CSS class. When the expression is false, Angular removes the selected class.

Read more about the [class] binding in the Template Syntax guide.

The final version of the <li> looks like this:

app.component.ts (styling each hero)
<li *ngFor="let hero of heroes"
  [class.selected]="hero === selectedHero"
  (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

After clicking "Magneta", the list should look like this:

Output of heroes list app

Here's the complete app.component.ts as of now:

src/app/app.component.ts
import { Component } from '@angular/core';

export class Hero {
  id: number;
  name: string;
}

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' }
];

@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <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}} details!</h2>
      <div><label>id: </label>{{selectedHero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ngModel)]="selectedHero.name" placeholder="name"/>
      </div>
    </div>
  `,
  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;
    }
  `]
})
export class AppComponent {
  title = 'Tour of Heroes';
  heroes = HEROES;
  selectedHero: Hero;

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
}

Summary

Here's what you achieved in this page:

  • The Tour of Heroes app displays a list of selectable heroes.
  • You added the ability to select a hero and show the hero's details.
  • You learned how to use the built-in directives ngIf and ngFor in a component's template.

Your app should look like this live example.

Next step

You've expanded the Tour of Heroes app, but it's far from complete. An app shouldn't be one monolithic component. In the next page, you'll split the app into subcomponents and make them work together.

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v4.angular.io/tutorial/toh-pt2