Master/Detail

展示更多英雄

我们的故事需要更多英雄。我们将会扩展英雄之旅app来展示一个英雄列表,允许用户选择一个英雄并且展示英雄的详细信息。

运行这个在线示例

让我们整理一下我们所需要展示的英雄列表。首先,我们需要一个英雄列表。我们希望在视图模板中展示那些英雄,因此我们将需要一个可行的方法实现它。

从哪里开始

在我们进行英雄之旅第2部分之前,让我们先验证一下第1部分的文件结构。如果不对,我们需要返回上一部分查找缺失了什么。

angular2-tour-of-heroes
├── app
│   ├── app.component.ts
│   └── main.ts
├── node_modules ...
├── typings ...
├── index.html
├── package.json
├── styles.css
├── systemjs.config.js
├── tsconfig.json
└── typings.json

保持app运行

我们想要运行TypeScript编译器,让它监控文件变更,并且运行服务器。为此我们将执行命令:

npm start

这个命令将运行监控模式的编译器,启动服务器,在浏览器中打开app,并保证app在我们不断创建英雄之旅时运行。

展示我们的英雄

创建英雄

让我们在app.component.ts底部创建一个包含10个英雄的数组。

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

这个HEROES数组的类型为Hero, 它定义在第1部分。我们本希望从一个web service中获取英雄列表,但让我们先简单用模拟的英雄数据进行展示。

暴露英雄

让我们在AppComponent中创建一个公共属性用来绑定英雄列表。

public heroes = HEROES;

我们不需要定义heroes类型,TypeScript可以从HEROES数组中继承类型。

我们可以在组件类中定义英雄列表,但是我们知道最终我们将会从一个数据service获取英雄数据。由于我们知道将来会如何改动,所以在一开始将英雄数据定义与类实现分离是有意义的。

在template中展示英雄数据

现在我们的组件有heroes了,让我们在template中创建一个无序列表展示它们。我们将会在title与hero详情之间插入以下html代码片段。

<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>

现在我们可以填充我们的英雄了。

使用ngFor列举英雄

我们希望在组件中把heroes数据与template绑定,迭代它们,并且分别展示。我们需要Angular来帮助完成这些。让我们一步一步进行。

首先修改<li>标签,添加内置指令*ngFor

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

ngFor前面的的星号(*)是语法中极其重要的部分。

ngFor之前的*暗示<li>元素和它的子元素构成一个主模板。

ngFor指令迭代AppComponent.heroes属性返回的heroes数组。

赋值给ngFor的引号中的文内容意思是“获取heroes数组中的每一个hero,保存在localhero变量中,并且使之让相对应的模板实例可访问。”

“hero”之前的let关键字标识hero作为一个模板输入变量。我们可以在模板引用这个变量来访问hero的属性。

想要学习更多关于ngFor和模板输入变量只是,请访问Displaying DataTemplate Syntax章节。

现在我们来在<li>标签之间插入一些内容,它将使用hero模板变量来展示hero的属性。

<li *ngFor="let hero of heroes">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

刷新浏览器后,我们就能看到英雄列表了!

为英雄添加样式

我们的英雄列表看起来相当单调。当我们将鼠标悬停在上面以及选择英雄的时候,我们想要使之在视觉上对用户更加显眼。

让我们在@Component装饰器中通过设置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;
  }
`]

注意到我们又使用了反引号表示多行字符串。

这些样式太多了!我们可以把它们移到单独的文件中使组件更简洁,我们将在后面的章节做这件事,现在让我们保持这样。

当我们样式赋值给一个组件,它们就仅仅在特定的组件域中可见。这些样式只会对AppComponent起作用,而不会“泄漏”到外部html。

现在我们展示英雄的template看起来是这样的:

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

选择一个英雄

现在我们有一个英雄列表以及单个英雄展示在app上。这个列表和单个的英雄并没有任何的联系。我们希望用户从列表中选择一个英雄,并且让这个选中的英雄展示在详情视图中。这个UI模式就是众所周知的“主从模式”。在我们的示例中,主即英雄列表,从即选中的英雄详情。

现在让我们通过将selectedHero组件属性绑定到一个鼠标点击事件来将主从关联起来。

点击事件

我们来修改<li>元素,插入一个绑定到点击事件的Angular事件

<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
  <span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

关注下事件绑定

(click)="onSelect(hero)"

小括号标识出<li>元素的click事件作为绑定目标。表达式等号右边调用了AppComponent的方法onSelect(),同时传递了模板输入变量hero作为参数。它与我们之前定义在ngFor中的hero变量是一致的。

想要学习更多关于时间绑定的只是,请访问User InputTemplating Syntax章节。

添加鼠标点击处理程序

事件所绑定的onSelect方法尚未添加,我们现在要把这个方法添加到组件中。

那么这个方法应该做些什么呢?它会设置组件选中的hero为用户所点击的那一个。

我们的组件尚未拥有一个“选中的英雄”,我们将从这里入手。

暴露选中的英雄

我们现在不需要AppComponenthero静态属性了,把它替换为下面这个简单的selectedHero属性:

selectedHero: Hero;

我们已经明确在用户选择一个英雄之前不默认设置选中的英雄,因此我们就不再像hero那样初始化selectedHero了。

现在添加一个onSelect方法,在其中设置selectedHero属性为用户点击的hero

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

接着我们将在template中展示选中英雄的详情。此时,template仍然引用着老的hero属性,让我们修改template使之绑定到新的selectedHero属性。

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

使用ngIf隐藏空详情

当app加载后,我们看到一个英雄列表,但并没有选中任何英雄,此时selectedHeroundefined。这就是为什么我们会在浏览器console中看到如下错误:

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

记得之前我们在template展示了selectedHere.name,然而由于selectedHero自身是undefined,name属性并不存在。

我们将通过保持英雄详情在DOM之外直到显式选择一个英雄来解决这个问题。

让我们来用一个<div>包裹英雄详情模板的html。接着添加内置指令ngIf,并且赋值为组件的selectedHero属性。

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

记住ngIf前面的星号(*)是语法中极其重要的部分。

selectedHero没有赋值时,ngIf指令从DOM中移除了英雄详情html。这样就不会有英雄详情页面元素,也不用担心没有绑定项了。

当用户选中一个英雄,selectedHero变为"true",ngIf把英雄详情内容放入DOM中,并且为赋值给嵌套的绑定项。

ngIfngFor都叫做“结构性指令”, 因为他们可以改变DOM的部分结构。换句话说,它们把结构给了Angular,使之在DOM展示内容。

想要学习更多关于ngIfngFor和其他结构性指令的知识,请访问Structural DirectivesTemplate Syntax章节。

刷新浏览器我们会看到英雄列表单而不是英雄详情。只要selectedHero为undefined,ngIf就会确保它在DOM之外。当我们点击列表中的一个英雄时,英雄详情视图就会显示选中的英雄信息。一切都如我们期望的那样运行。

给选中项添加样式

我们可以看到下方详情区域里选中的英雄,但是我们并不能在上方列表中快速定位相对应的英雄。我们可以通过给对应的<li>附加selectedcss class来修复这个问题。比如,当我们选中列表中的Magneta,我们可以通过设置背景色使之视觉上凸显,如下图所示。

heroes-list-selected

我们将在template中为selectedclass添加一个关联到class的属性绑定。我们把这个属性赋值为一个表达式,将当前的selectedHerohero比较。

key是CSS class的名称(selected)。如果这两个hero相匹配,那么值为true,反之为false。也就是说“如果hero相匹配则添加selectedclass,反之则移除。”

[class.selected]="hero === selectedHero"

注意到template中的class.selected包裹在中括号([])里。这是一个属性绑定的语法,这个绑定使得数据从数据源(表达式hero===selectedHero)单向流动到class的一个属性中。

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

想要学习更多关于Property Bingdings的知识,请访问Template Syntax章节

刷新浏览器,我们选择英雄Magneta后,选中项就会清楚地被背景色标识出来。

heroes-list-1

现在完整的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) { this.selectedHero = hero; }
}

回顾历程

本章我们达成了如下目标:

  • 英雄之旅现在能显示一个可选择的英雄俩表
  • 我们添加了选中英雄并展示英雄详情的功能
  • 我们学习了如何在template中使用内置指令ngIfngFor

运行在线实例来演示这一部分。

results matching ""

    No results matching ""