Dart和Angular2的英雄之旅(3):主从结构

显示多个英雄

我们的故事需要更多的英雄。让我们来扩展《英雄指之旅》,让它显示一个英雄列表,并允许用户选择一个英雄,查看该英雄的详细信息。

运行这部分的 在线实例下载源码

我们来考虑一下显示英雄列表都需要些什么。首先,需要一个英雄列表。我们要在视图的模板中显示这些英雄,因此我们需要一种方式来做到这一点。

我们离开的地方

在继续《英雄之旅》的第二部分之前,先来检查一下,在完成 第一部分 后项目结构如下。如果不是,你得先回到第一部分,并看看错过了什么。

保持应用的编译和运行

我们需要启动Dart编译器,它会监视文件变更,并且启动服务器。我们只要敲入:

pub serve

在我们继续构建《英雄之旅》的时候,这个命令会让应用得以持续运行。

显示我们的英雄

创建英雄

我们在 app_component.dart 下面,先创建一个由十位英雄组成的列表。

final List<Hero> mockHeroes = [
  new Hero(11, 'Mr. Nice'),
  new Hero(12, 'Narco'),
  new Hero(13, 'Bombasto'),
  new Hero(14, 'Celeritas'),
  new Hero(15, 'Magneta'),
  new Hero(16, 'RubberMan'),
  new Hero(17, 'Dynama'),
  new Hero(18, 'Dr IQ'),
  new Hero(19, 'Magma'),
  new Hero(20, 'Tornado')
];

mockHeroes 是一个由 Hero 类型组成的列表。我们希望从一个 Web 服务中获取这个英雄列表,但别急,我们先迈出第一步,显示模拟的英雄。

暴露英雄接口

我们在 AppComponent 上创建一个公开属性,用来暴露这些英雄,用于绑定。

final List<Hero> heroes = mockHeroes;

我们已经把英雄列表定义在了这个组件类中。但显然,我们最终还是得从一个数据服务中获取这些英雄。正因如此,我们从一开始就要有意识的把英雄数据隔离到一个类中来实现。

在模板中显示英雄

我们的组件有了 heroes 属性,我们再到模板中创建一个无序列表来显示它们。我们将在标题和英雄详情之间,插入下面这段 HTML 代码。

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

现在,我们有了一个模板,我们可以用我们的英雄来填充它。

通过 ngFor 来列出英雄

我们希望把组件中的 heroes 列表绑定到模板中,迭代并分别显示它们。我们需要借助 Angular 来完成它了。我们一步步来!

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

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

注:ngFor 前面的星号 *  是此语法的重要组成部分。

ngFor 的星号 * 前缀表示 <li> 及其子元素组成了一个主控模板。

ngFor 指令会迭代 AppComponent.heroes 属性返回的 heroes 列表,并输出此模板的实例。

引号中赋值给 ngFor 的那段文本表示“从 heroes 列表中取出每个英雄,存入一个局部的 hero 变量,并让它在相应的模板实例中可用”。

hero 前的 let 关键字表示 hero 是一个模板输入变量。在模板中,我们可以引用这个变量来访问一位英雄的属性。

关于更多 ngFor 和模板输入变量的内容,参见 显示数据模板语法 两章。

现在,我们在 <li> 标签中插入一些内容——使用模板变量 hero 来显示英雄的属性。

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

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

给英雄们添加样式

我们的英雄列表看起来实在是稀松平常。但当用户的鼠标划过英雄或选中了一个英雄时,我们得让它看起来醒目一点。

让我们给组件添加一些样式。将注解@Componentstyles 属性设置为下列 CSS 类:

styles: const [
  '''
  .selected {
    background-color: #CFD8DC !important;
    color: white;
  }
  .heroes {
    margin: 0 0 2em 0;
    list-style-type: none;
    padding: 0;
    width: 10em;
  }
  .heroes li {
    cursor: pointer;
    position: relative;
    left: 0;
    background-color: #EEE;
    margin: .5em;
    padding: .3em 0em;
    height: 1.6em;
    border-radius: 4px;
  }
  .heroes li.selected:hover {
    color: white;
  }
  .heroes li:hover {
    color: #607D8B;
    background-color: #EEE;
    left: .1em;
  }
  .heroes .text {
    position: relative;
    top: -3px;
  }
  .heroes .badge {
    display: inline-block;
    font-size: small;
    color: white;
    padding: 0.8em 0.7em 0em 0.7em;
    background-color: #607D8B;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -4px;
    height: 1.8em;
    margin-right: .8em;
    border-radius: 4px 0px 0px 4px;
  }
'''
])

注意,我们又使用了三引号语法来编写多行字符串。

样式也太多了!其实,我们可以像上面那样将它们内联到模板中,或者将样式移动到它们自己的文件中,以便于编写组件。后面的章节中我们会这样处理,这里我们先不管它。

当我们把样式赋予一个组件时,它们的作用域将仅限于该组件。也就是说,上面的样式只会作用于 AppComponent 组件,而不会“泄露”到外部 HTML 中。

现在,用于显示英雄的模板看起来是这样的:

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

选择英雄

在我们的应用中,已经有了英雄列表页以及一个单独的英雄显示页。但列表和单独的英雄之间还没有任何关联。我们希望用户在列表中选中一个英雄,然后让这个被选中的英雄出现在详情视图中。这种 UI 布局模式,通常被称为“主从视图”。 在这个例子中,主视图是英雄列表,从视图则是被选中的英雄。

我们将组件属性 selectedHero 与click事件相关联,以便连接主从视图。

click 事件

我们通过插入Angular事件(并非HTML事件)来修改 <li> ,以绑定元素的click事件。

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

关注一下事件绑定

(click)="onSelect(hero)"

圆括号会识别目标事件—— <li> 元素的 click 事件。等号右边的表达式调用 AppComponentonSelect() 方法,并把模板输入变量 hero 作为参数传进去。 它和我们前面在 ngFor 中定义的 hero 变量是同一个。

关于更多事件绑定的内容,参见: 用户输入模板语法 两章。

添加 click 事件处理器

我们的事件绑定引用了 onSelect 方法,但它还不存在。 我们现在就把它添加到组件上。

这个方法该做什么?它应该把组件中“当前选中的英雄”设置为用户刚刚点击的那个。

我们的组件还没有用来表示“当前选中的英雄”的变量,我们就从这一步开始。

暴露当前选中的英雄

AppComponent 上,我们不再需要静态的 hero 属性。 将它替换为单纯的 selectedHero 属性。

Hero selectedHero;

我们已决定:在用户选中一个英雄之前,不应该选中任何英雄。所以,我们不用像 hero 一样去初始化 selectedHero 变量。

现在, 添加一个 onSelect 方法 ,将用户点击的 hero 赋给 selectedHero 属性。

onSelect(Hero hero) {
  selectedHero = hero;
}

我们将把所选英雄的详细信息显示在模板中。目前,它仍然引用的是以前的 hero 属性。 我们这就修改模板,让它绑定到新的 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 隐藏空的详情

当应用刚加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。 selectedHero 属性是 undefined 状态。因此,我们会在浏览器控制台中看到下列错误:

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

记住,我们要在模板中显示的是 selectedHero.name 。 显然这个 name 属性是不存在的,因为 selectedHero 本身还是 undefined 状态。

如果要处理这个问题,那么在有英雄被选中前,应该让英雄详情不出现在 DOM 中。

我们将模板中的“英雄详情”内容区用 <div> 封装起来。然后添加内置指令 ngIf ,然后把值设置为组件的 selectedHero 属性。

<div *ngIf="selectedHero != null">
  <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 null 时, ngIf 指令会从 DOM 中移除表示英雄详情的这段 HTML 。没有了表示英雄详情的元素,也就不用担心绑定问题。

当用户选取中一个英雄时, selectedHero 不再是 null ,于是 ngIf 把“英雄详情”加回 DOM 中,并且对嵌套的各种绑定进行求值计算。

注: ngIfngFor 被称为“结构型指令”,因为它们可以修改 DOM 的部分结构。 换句话说,它们让 Angular 在 DOM 中以结构化的方式来显示内容。

关于更多 ngIfngFor 和其它结构型指令的内容,参见: 结构型指令模板语法 两章。

刷新浏览器,我们看到了一个英雄列表,但是还没有选中的英雄详情。 当 selectedHero 是 undefined 时, ngIf 会保证英雄详情不出现在 DOM 中。 当我们点击列表中的英雄时,选中的英雄被显示在英雄详情中。一切正如我们所预期的那样。

给选中的英雄添加样式

我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表迅速定位英雄。 通过把 CSS 类的 selected 添加到主列表适当的 <li> 元素上,我们可以解决这个问题。 比如,当我们在列表区选中了 Magneta 时,我们可以通过设置一个轻微的背景色来让它略显突出。

我们将在 classselected 类上,添加一个属性绑定到模板上。并将绑定设置为表达式:比较当前 selectedHerohero

关键是 CSS 类的名字: selected 。当两位英雄一致时,它为 true ,否则为 false 。 也就是说:“ 当两位英雄相同时,应用 selected 类,否则删除类 ”。

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

注意,模板中的 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>

关于更多 属性绑定 的内容,参见 模板语法 的章节。

浏览器重新加载应用。我们选中英雄 Magneta ,然后通过背景色的变化,它被清晰的标记了出来。

我们选择了另一个英雄,于是色标也跟着移到了这位英雄上。

完整的 app_component.dart 文件如下:

import 'package:angular2/core.dart';

class Hero {
  final int id;
  String name;
  Hero(this.id, this.name);
}
final List<Hero> mockHeroes = [
  new Hero(11, 'Mr. Nice'),
  new Hero(12, 'Narco'),
  new Hero(13, 'Bombasto'),
  new Hero(14, 'Celeritas'),
  new Hero(15, 'Magneta'),
  new Hero(16, 'RubberMan'),
  new Hero(17, 'Dynama'),
  new Hero(18, 'Dr IQ'),
  new Hero(19, 'Magma'),
  new Hero(20, '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 != null">
        <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: const [
      '''
      .selected {
        background-color: #CFD8DC !important;
        color: white;
      }
      .heroes {
        margin: 0 0 2em 0;
        list-style-type: none;
        padding: 0;
        width: 10em;
      }
      .heroes li {
        cursor: pointer;
        position: relative;
        left: 0;
        background-color: #EEE;
        margin: .5em;
        padding: .3em 0em;
        height: 1.6em;
        border-radius: 4px;
      }
      .heroes li.selected:hover {
        color: white;
      }
      .heroes li:hover {
        color: #607D8B;
        background-color: #EEE;
        left: .1em;
      }
      .heroes .text {
        position: relative;
        top: -3px;
      }
      .heroes .badge {
        display: inline-block;
        font-size: small;
        color: white;
        padding: 0.8em 0.7em 0em 0.7em;
        background-color: #607D8B;
        line-height: 1em;
        position: relative;
        left: -1px;
        top: -4px;
        height: 1.8em;
        margin-right: .8em;
        border-radius: 4px 0px 0px 4px;
      }
    '''
    ])
class AppComponent {
  final String title = 'Tour of Heroes';
  final List<Hero> heroes = mockHeroes;
  Hero selectedHero;
  onSelect(Hero hero) {
    selectedHero = hero;
  }
}

我们已经走过的路

我们在本章中实现了:

  • 我们的《英雄之旅》现在可以显示一个可选的英雄列表
  • 我们添加了选择英雄的功能,并且会显示英雄的详细信息
  • 我们学会了在组件的模板中,如何使用内置的 ngIfngFor 指令

前方的路

我们的《英雄之旅》成长了不少,但还远远不够。我们不应把整个应用都放在一个组件中。我们需要把它拆分成一系列子组件,然后让它们协同工作——这也是我们学习的 下一章 的内容。

本文出自“Dart语言中文社区”,允许转载,转载时请务必以超链接形式标明文章原始出处
本文地址:
http://www.cndartlang.com/813.html

发表评论

登录后才能评论