英雄指南——主从结构

版本:v4.0.0+2

在这一章,你将扩展英雄指南应用来显示一列英雄,并允许用户来选择一个英雄,同时显示这个英雄的详情。

当你按照本章完成时,应用应该看起来这样——在线示例 (查看源码)。

我们离开的地方

在继续本章的英雄指南之前,先来检查一下是否有下面的结构。如果不是,你得先回到前一章英雄编辑器,看看错过了什么。

如果应用不运行了,启动应用。当你做出修改时,通过刷新浏览器保持继续运行。

应用重构

在添加新的应用之前,你将从重构应用标题受益。

应用模板文件

你将对 app 组件的模板做几个更新。首先,移动模板到自己的文件:

// lib/app_component.html

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

使用templateUrl引用新的模板文件替换@Component template

// lib/app_component.dart (metadata)

@Component(
  selector: 'my-app',
  templateUrl: 'app_component.html',
  directives: const [formDirectives],
)

刷新浏览器。应用仍然运行。

英雄类

app_component.dart分离Hero类到它自己的文件。

创建lib/src目录包含Hero资源:

// lib/src/hero.dart

class Hero {
  final int id;
  String name;

  Hero(this.id, this.name);
}

回到 app 组件,使用相对路径添加新创建文件的导入:

// lib/app_component.dart (hero import)

import 'src/hero.dart';

刷新浏览器。应用仍然运行,现在开始准备添加新的特性。

显示英雄

要显示一列英雄,添加 heroes 到视图模板。

模拟英雄

lib/src目录下创建如下一个由十位英雄组成的列表的文件。

// lib/src/mock_heroes.dart

import 'hero.dart';

final mockHeroes = <Hero>[
  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')
];

在最终,应用会从一个 web 服务器获取英雄列表,但现在你可以显示模拟应用。

应用的 heroes 字段

AppComponent中使用公共的heroes字段替换hero字段,并使用模拟英雄(不要忘记导入)初始化它:

// lib/app_component.dart (heroes)

import 'src/mock_heroes.dart';

// ···
class AppComponent {
  final title = 'Tour of Heroes';
  List<Hero> heroes = mockHeroes;
  // ···
}

英雄数据是由分开的不同类实现的,因为最终,英雄名将会来自服务器数据。

在模板中显示英雄名

在一个无序列表中显示英雄名,使用下面的 HTML 代替 所有当前模板:

// lib/app_component.html (heroes template)

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

下一步,你将添加英雄名。

使用 ngFor 来列出英雄

目标是绑定组件的 heroes 列表到模板,迭代它们,并单独显示它们。

修改<li>标签,添加核心指令 *ngFor

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

ngFor的前缀星号(*)是这个语法的关键部分。它表明<li>元素及其子元素组成了一个主控模板。

ngFor指令遍历组件的heroes列表,并按照这个模板渲染列表中每个英雄。

表达式的let hero部分识别hero为模板输入变量,为每一个迭代保存当前英雄条目。你可以在模板中引用这个变量来访问当前英雄的属性。

更多ngFor和模板输入变量的内容请看显示数据使用 ngFor 显示属性列表模板语法ngFor 部分。

<li>元素内,使用模板变量hero来添加内容,显示英雄的属性。

// lib/app_component.html (ngFor)

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

要在模板中使用 Angular 指令,需要在组件的@Component注解的directives参数中列出它们。类似于在前一章做的,添加 CORE_DIRECTIVES:

// lib/app_component.dart (directives)

@Component(
  selector: 'my-app',
  // ···
  directives: const [CORE_DIRECTIVES, formDirectives],
)

刷新浏览,一列英雄出现了。

给英雄们添加样式

用户应该获取视觉提示,他们悬停在哪个英雄上,哪个英雄被选中。

通过设置@Component注解的styles参数,来给组件添加样式:

// lib/app_component.dart (styles)

// 当添加许多 CSS 类时不推荐
styles: const [
  '''
    .selected { ... }
    .heroes { ... }
    ...
  '''
],

但是当添加许多样式时,会使 Dart 文件变长且难以阅读。相反,把样式放到一个.css文件中,然后在@ComponentstyleUrls参数中引用该文件。按照惯例,组件的 CSS 文件名和 Dart 文件有相同的基础(app_component)。

//  lib/app_component.css

.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 {
  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 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。

显示英雄的模板看起来像这样:

// lib/app_component.html (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>

选择英雄

应用现在显示一列英雄,同时在详情视图显示一个单独的英雄。但列表和详情视图之间没有关联。当用户从列表选中一个英雄时,选中的英雄应该出现在详情视图中。这种 UI 模式被称为“master/detail.”。在这个例子中,master 是英雄列表,detail 是被选中的英雄。

接下来,通过组件的selectedHero属性来连接主从视图,它被绑定到一个点击事件。

处理点击事件

如下添加一个点击事件绑定到<li>元素:

// lib/app_component.html (click)

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

圆括号指定了<li>元素的click事件为目标。onSelect(hero)表达式调用AppComponentonSelect()方法,它传递模板输入变量hero作为参数。和之前在ngFor指令中定义的是同一个hero变量。

学习更多关于事件绑定的内容,请看用户输入模板语法事件绑定 部分。

添加一个点击处理器来暴露选中的英雄

你不再需要hero属性,因为你不再显示一个单独的英雄;你显示一列英雄。但用户能够通过点击它来选择其中一个英雄。所以使用这个简单的selectedHero属性代替hero属性。

// lib/app_component.dart (selectedHero)

Hero selectedHero;

在用户选择一个英雄之前,所有英雄都是未被选中的,所以,你不需要像hero一样初始化selectedHero

添加一个onSelect()方法,将用户点击的hero赋值给electedHero属性。

// lib/app_component.dart (onSelect)

void onSelect(Hero hero) => selectedHero = hero;

模板仍然引用旧的hero属性。如下所示,绑定到新的selectedHero属性代替它:

// lib/app_component.html (selectedHero details)

<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是 null。当用户点击英雄名字的时候,selectedHero才会被初始化。Angular 不能显示空的selectedHero的属性,并且抛出如下的错误,可以在浏览器控制台中查看:

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

尽管selectedHero.name显示在了模板中,但在有一个选中的英雄之前,英雄详情不会出现在 DOM 中。

模板中的英雄详情 HTML 内容使用一个<div>包裹起来。然后,添加ngIf核心指令,并把它设置为selectedHero != null

// lib/app_component.html (ngIf)

<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 的前缀(*)。

刷新浏览器。应用不再失败,并且一列名字再次显示在浏览器中。

当没有选中英雄时,ngIf指令从 DOM 中移除英雄详情 HTML。没有了英雄详情元素,也就不用担心绑定问题。

当用户选中一个英雄,selectedHero不再是nullngIf把英雄详情添加到 DOM 中,并且对嵌套的绑定进行求值计算。

更多关于ngIfngFor的内容请看结构指令模板语法内置指令 部分。

给选中英雄添加样式

虽然选中英雄的详情出现在了列表下面,但很难在列表中识别出选中的英雄。

在之前添加的styles元数据中,有一个自定义的 CSS 类selected。要使选中英雄更明显,当用户点击一个英雄名时,把selected类应用到<li>上。例如,当用户点击 “Magneta” 时,它应该用一个略有不同的背景色显示出来,就像这样:

在模板中,如下所示,添加绑定到<li>标签:

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

当表达式(hero === selectedHero)是true时,Angular 添加selectedCSS 类。当表达式是false,Angular 移除selected类。

===操作符测试给出的对象是否完全相等

更多关于[class]绑定的内容请看模板语法指南。

最终版的<li>看起来如下:

// lib/app_component.html (ngFor with class.selected)

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

点击 “Magneta” 后,列表看起来如下:

回顾应用结构

你的应用应该有如下文件:

教程测试组件

本教程没有包含测试,如果你查看示例代码,它有对本教程添加的每个新特性的组件测试。查看 Component Testing 了解更多信息。

你已走过的路

在本页你完成了以下内容:

  • 英雄指南应用显示一列可选英雄。
  • 移动应用的模板到它自己的文件。
  • 移动Hero类到lib/src下它自己的文件。
  • 添加选择英雄并且显示英雄详情的功能。
  • 学习在组件的模板中,如何使用核心指令ngIfngFor
  • 在 CSS 文件中定义样式,并使用它们给应用添加样式。

你的应用看起来应该这样——在线示例 (查看源码)。

下一步

多组件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 版本:4.0.0+2 此时AppComponent做了所有的事情。起初,它显示一个单一英雄的详情。然后,它成了有一...
    soojade阅读 580评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Angular 2架构总览 - 简书http://www.jianshu.com/p/aeb11061b82c A...
    葡萄喃喃呓语阅读 1,482评论 2 13
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,825评论 25 707
  • 天本来就灰蒙蒙的,又不知被谁溅上一道恶心的红颜色。 放学铃声自作多情地唠叨了好久。 飞奔到校门口才终于听不见。 路...
    治百病的草阅读 387评论 0 1