填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

顯示英雄列表

Display a selection 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.

要檢視本頁所講的範例程式,參閱現場演練 / 下載範例

For the sample app that this page describes, see the現場演練 / 下載範例.

建立模擬(mock)的英雄資料

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.

src/app/ 資料夾中建立一個名叫 mock-heroes.ts 的檔案。 定義一個包含十個英雄的常量陣列 HEROES,並匯出它。 該檔案是這樣的。

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: 'Dr 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' } ];
src/app/mock-heroes.ts
      
      import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 11, name: 'Dr 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

開啟 HeroesComponent 類別檔案,並匯入模擬的 HEROES

Open the HeroesComponent class file and import the mock HEROES.

import { HEROES } from '../mock-heroes';
src/app/heroes/heroes.component.ts (import HEROES)
      
      import { HEROES } from '../mock-heroes';
    

往類別中新增一個 heroes 屬性,這樣可以暴露出這個 HEROES 陣列,以供繫結。

In the same file (HeroesComponent class), define a component property called heroes to expose the HEROES array for binding.

export class HeroesComponent implements OnInit { heroes = HEROES; }
src/app/heroes/heroes.component.ts
      
      export class HeroesComponent implements OnInit {

  heroes = HEROES;
}
    

使用 *ngFor 列出這些英雄

List heroes with *ngFor

開啟 HeroesComponent 的範本檔案,並做如下修改:

Open the HeroesComponent template file and make the following changes:

  • 在頂部新增 <h2>

    Add an <h2> at the top,

  • 然後新增表示無序列表的 HTML 元素(<ul>

    Below it add an HTML unordered list (<ul>)

  • <ul> 中插入一個 <li> 元素,以顯示單個 hero 的屬性。

    Insert an <li> within the <ul> that displays properties of a hero.

  • 點綴上一些 CSS 類別(稍後你還會新增更多 CSS 樣式)。

    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>
heroes.component.html (heroes template)
      
      <h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>
    

這只展示了一個英雄。要想把他們都列出來,就要為 <li> 新增一個 *ngFor 以便迭代出列表中的所有英雄:

That shows one hero. To list them all, add an *ngFor to the <li> to iterate through the list of heroes:

<li *ngFor="let hero of heroes">
      
      <li *ngFor="let hero of heroes">
    

*ngFor是一個 Angular 的複寫器(repeater)指令。 它會為列表中的每項資料複寫它的宿主元素。

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

這個例子中涉及的語法如下:

The syntax in this example is as follows:

  • <li> 就是 *ngFor 的宿主元素。

    <li> is the host element.

  • heroes 就是來自 HeroesComponent 類別的列表。

    heroes holds the mock heroes list from the HeroesComponent class, the mock heroes list.

  • 當依次遍歷這個列表時,hero 會為每個迭代儲存當前的英雄物件。

    hero holds the current hero object for each iteration through the list.

不要忘了 ngFor 前面的星號(*),它是該語法中的關鍵部分。

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.

課程的第一章,你曾在 styles.css 中為整個應用設定了一些基礎的樣式。 但那個樣式表並不包含英雄列表所需的樣式。

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.

固然,你可以把更多樣式加入到 styles.css,並且放任它隨著你新增更多元件而不斷膨脹。

You could add more styles to styles.css and keep growing that stylesheet as you add components.

但還有更好的方式。你可以定義屬於特定元件的私有樣式,並且讓元件所需的一切(程式碼、HTML 和 CSS)都放在一起。

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.

你可以用多種方式定義私有樣式,或者內聯在 @Component.styles 陣列中,或者在 @Component.styleUrls 所指出的樣式表文件中。

You define private styles either inline in the @Component.styles array or as stylesheet file(s) identified in the @Component.styleUrls array.

當 CLI 產生 HeroesComponent 時,它也同時為 HeroesComponent 建立了空白的 heroes.component.css 樣式表文件,並且讓 @Component.styleUrls 指向它,就像這樣:

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'] })
src/app/heroes/heroes.component.ts (@Component)
      
      @Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
    

開啟 heroes.component.css 檔案,並且把 HeroesComponent 的私有 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.

@Component 元資料中指定的樣式和樣式表都是侷限於該元件的。 heroes.component.css 中的樣式只會作用於 HeroesComponent,既不會影響到元件外的 HTML,也不會影響到其它元件中的 HTML。

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.

主從結構

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.

新增 click 事件繫結

Add a click event binding

再往 <li> 元素上插入一句點選事件的繫結程式碼:

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

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

這是 Angular 事件繫結 語法的例子。

This is an example of Angular's event binding syntax.

click 外面的圓括號會讓 Angular 監聽這個 <li> 元素的 click 事件。 當用戶點選 <li> 時,Angular 就會執行表示式 onSelect(hero)

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.

下一部分,會在 HeroesComponent 上定義一個 onSelect() 方法,用來顯示 *ngFor 表示式所定義的那個英雄(hero)。

In the next section, define an onSelect() method in HeroesComponent to display the hero that was defined in the *ngFor expression.

新增 click 事件處理器

Add the click event handler

把該元件的 hero 屬性改名為 selectedHero,但不要為它賦值。 因為應用剛剛啟動時並沒有所選英雄

Rename the component's hero property to selectedHero but don't assign it. There is no selected hero when the application starts.

新增如下 onSelect() 方法,它會把範本中被點選的英雄賦值給元件的 selectedHero 屬性。

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; }
src/app/heroes/heroes.component.ts (onSelect)
      
      selectedHero: Hero;
onSelect(hero: Hero): void {
  this.selectedHero = hero;
}
    

新增詳情區

Add a details section

現在,元件的範本中有一個列表。要想點選列表中的一個英雄,並顯示該英雄的詳情,你需要在範本中留一個區域,用來顯示這些詳情。 在 heroes.component.html 中該列表的緊下方,新增如下程式碼:

Currently, you have a list in the component template. To click on a hero on the list and reveal details about that hero, you need a section for the details to render in the template. Add the following to heroes.component.html beneath the list section:

<h2>{{selectedHero.name | uppercase}} Details</h2> <div><span>id: </span>{{selectedHero.id}}</div> <div> <label>name: <input [(ngModel)]="selectedHero.name" placeholder="name"/> </label> </div>
heroes.component.html (selected hero details)
      
      <h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
  <label>name:
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
  </label>
</div>
    

重新整理瀏覽器,應用掛了。

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
      
      HeroesComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined
    

怎麼回事?

What happened?

當應用啟動時,selectedHeroundefined設計如此

When the app starts, the selectedHero is undefined by design.

但範本中的繫結表示式參考了 selectedHero 的屬性(表示式為 {{selectedHero.name}}),這必然會失敗,因為你還沒選過英雄呢。

Binding expressions in the template that refer to properties of selectedHero—expressions like {{selectedHero.name}}must fail because there is no selected hero.

修復 —— 使用 *ngIf 隱藏空白的詳情

The fix - hide empty details with *ngIf

該元件應該只有當 selectedHero 存在時才顯示所選英雄的詳情。

The component should only display the selected hero details if the selectedHero exists.

把顯示英雄詳情的 HTML 包裹在一個 <div> 中。 並且為這個 div 新增 Angular 的 *ngIf 指令,把它的值設定為 selectedHero

Wrap the hero detail HTML in a <div>. Add Angular's *ngIf directive to the <div> and set it to selectedHero.

不要忘了 ngIf 前面的星號(*),它是該語法中的關鍵部分。

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>
src/app/heroes/heroes.component.html (*ngIf)
      
      <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 in the list of heroes and its details appear. 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.

為什麼改好了?

Why it works

selectedHeroundefined 時,ngIf 從 DOM 中移除了英雄詳情。因此也就不用關心 selectedHero 的綁定了。

When selectedHero is undefined, the ngIf removes the hero detail from the DOM. There are no selectedHero bindings to consider.

當用戶選擇一個英雄時,selectedHero 也就有了值,並且 ngIf 把英雄的詳情放回到 DOM 中。

When the user picks a hero, selectedHero has a value and ngIf puts the hero detail into the DOM.

給所選英雄新增樣式

Style the selected hero

所有的 <li> 元素看起來都是一樣的,因此很難從列表中識別出所選英雄

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

如果使用者點選了“Magneta”,這個英雄應該用一個略有不同的背景色顯示出來,就像這樣:

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

所選英雄的顏色來自於你前面新增的樣式中的 CSS 類別 .selected。 所以你只要在使用者點選一個 <li> 時把 .selected 類別應用到該元素上就可以了。

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.

Angular 的 CSS 類別繫結機制讓根據條件新增或移除一個 CSS 類別變得很容易。 只要把 [class.some-css-class]="some-condition" 新增到你要施加樣式的元素上就可以了。

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.

HeroesComponent 範本中的 <li> 元素上新增 [class.selected] 繫結,程式碼如下:

Add the following [class.selected] binding to the <li> in the HeroesComponent template:

[class.selected]="hero === selectedHero"
heroes.component.html (toggle the 'selected' CSS class)
      
      [class.selected]="hero === selectedHero"
    

如果當前行的英雄和 selectedHero 相同,Angular 就會新增 CSS 類別 selected,否則就會移除它。

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.

最終的 <li> 是這樣的:

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>
heroes.component.html (list item hero)
      
      <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

下面是本頁面中所提及的程式碼檔案,包括 HeroesComponent 的樣式。

Here are the code files discussed on this page, including the HeroesComponent styles.

import { Hero } from './hero'; export const HEROES: Hero[] = [ { id: 11, name: 'Dr 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' } ];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 */ .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:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes li.selected { background-color: #CFD8DC; color: white; } .heroes li.selected:hover { background-color: #BBD8DC; color: white; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color:#405061; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; }
      
      import { Hero } from './hero';

export const HEROES: Hero[] = [
  { id: 11, name: 'Dr 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' }
];
    

小結

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.

  • 你使用 *ngFor 顯示了一個列表。

    You used *ngFor to display a list.

  • 你使用 *ngIf 來根據條件包含或排除了一段 HTML。

    You used *ngIf to conditionally include or exclude a block of HTML.

  • 你可以用 class 繫結來切換 CSS 的樣式類別。

    You can toggle a CSS style class with a class binding.