模板语法

Angular 应用管理着用户之所见和所为,并通过 Component 类的实例(组件)和面向用户的模板来与用户交互。

从使用模型-视图-控制器 (MVC) 或模型-视图-视图模型 (MVVM) 的经验中,很多开发人员都熟悉了组件和模板这两个概念。 在 Angular 中,组件扮演着控制器或视图模型的角色,模板则扮演视图的角色。

目录

本章涵盖了Angular模板语法中的基本元素,你在构建视图时会用到它们:

在线例子 / 可下载的例子演示了本章中描述的所有语法和代码片段。

模板中的HTML

HTML 是 Angular 模板的语言。几乎所有的HTML语法都是有效的模板语法。 但值得注意的例外是<script>元素,它被禁用了,以阻止脚本注入攻击的风险。(实际上,<script>只是被忽略了。) 参见安全页了解详情。

有些合法的 HTML 被用在模板中是没有意义的。<html><body><base>元素这个舞台上中并没有扮演有用的角色。剩下的所有元素基本上就都一样用了。

可以通过组件和指令来扩展模板中的 HTML 词汇。它们看上去就是新元素和属性。接下来将学习如何通过数据绑定来动态获取/设置 DOM(文档对象模型)的值。

我们首先看看数据绑定的第一种形式 —— 插值表达式,它展示了模板的 HTML 可以有多丰富。

回到顶部

插值表达式 ( {{...}} )

在以前的 Angular 教程中,我们遇到过由双花括号括起来的插值表达式,{{}}

<p>My current hero is {{currentHero.name}}</p>

插值表达式可以把计算后的字符串插入到 HTML 元素标签内的文本或对标签的属性进行赋值。

<h3>
  {{title}}
  <img src="{{heroImageUrl}}" style="height:30px">
</h3>

在括号之间的“素材”,通常是组件属性的名字。Angular 会用组件中相应属性的字符串值,替换这个名字。 上例中,Angular 计算titleheroImageUrl属性的值,并把它们填在空白处。 首先显示粗体的应用标题,然后显示英雄的图片。

一般来说,括号间的素材是一个模板表达式,Angular 先对它求值,再把它转换成字符串。 下列插值表达式通过把括号中的两个数字相加说明了这一点:

<!-- "The sum of 1 + 1 is 2" -->
<p>The sum of 1 + 1 is {{1 + 1}}</p>

这个表达式可以调用宿主组件的方法,就像下面用的getVal()

<!-- "The sum of 1 + 1 is not 4" -->
<p>The sum of 1 + 1 is not {{1 + 1 + getVal()}}</p>

Angular 对所有双花括号中的表达式求值,把求值的结果转换成字符串,并把它们跟相邻的字符串字面量连接起来。最后,把这个组合出来的插值结果赋给元素或指令的属性

表面上看,我们在元素标签之间插入了结果和对标签的属性进行了赋值。 这样思考起来很方便,并且这个误解很少给我们带来麻烦。 但严格来讲,这是不对的。插值表达式是一个特殊的语法,Angular 把它转换成了属性绑定后面将会解释这一点。

讲解属性绑定之前,先深入了解一下模板表达式和模板语句。

回到顶部

模板表达式

模板表达式产生一个值。 Angular 执行这个表达式,并把它赋值给绑定目标的属性,这个绑定目标可能是 HTML 元素、组件或指令。

{{1 + 1}}中所包含的模板表达式是1 + 1。 在属性绑定中会再次看到模板表达式,它出现在=右侧的引号中,就像这样:[property]="expression"

编写模板表达式所用的语言看起来很像 JavaScript。 很多 JavaScript 表达式也是合法的模板表达式,但不是全部。

JavaScript 中那些具有或可能引发副作用的表达式是被禁止的,包括:

和 JavaScript语 法的其它显著不同包括:

表达式上下文

典型的表达式上下文就是这个组件实例,它是各种绑定值的来源。 在下面的代码片段中,双花括号中的title和引号中的isUnchanged所引用的都是AppComponent中的属性。

{{title}}
<span [hidden]="isUnchanged">changed</span>

表达式的上下文可以包括组件之外的对象。 比如模板输入变量 (let hero)和模板引用变量(#heroInput)就是备选的上下文对象之一。

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<input #heroInput> {{heroInput.value}}

表达式中的上下文变量是由模板变量、指令的上下文变量(如果有)和组件的成员叠加而成的。 如果我们要引用的变量名存在于一个以上的命名空间中,那么,模板变量是最优先的,其次是指令的上下文变量,最后是组件的成员。

上一个例子中就体现了这种命名冲突。组件具有一个名叫hero的属性,而*ngFor声明了一个也叫hero的模板变量。 在{{hero.name}}表达式中的hero实际引用的是模板变量,而不是组件的属性。

模板表达式不能引用全局命名空间中的任何东西,比如windowdocument。它们也不能调用console.logMath.max。 它们只能引用表达式上下文中的成员。

回到顶部

表达式指南

模板表达式能成就或毁掉一个应用。请遵循下列指南:

超出上面指南外的情况应该只出现在那些你确信自己已经彻底理解的特定场景中。

没有可见的副作用

模板表达式除了目标属性的值以外,不应该改变应用的任何状态。

这条规则是 Angular “单向数据流”策略的基础。 永远不用担心读取组件值可能改变另外的显示值。 在一次单独的渲染过程中,视图应该总是稳定的。

执行迅速

Angular 执行模板表达式比我们想象的频繁。 它们可能在每一次按键或鼠标移动后被调用。 表达式应该快速结束,否则用户就会感到拖沓,特别是在较慢的设备上。 当计算代价较高时,应该考虑缓存那些从其它值计算得出的值。

非常简单

虽然也可以写出相当复杂的模板表达式,但不要那么写。

常规是属性名或方法调用。偶尔的逻辑取反 (!) 也还凑合。 其它情况下,应在组件中实现应用和业务逻辑,使开发和测试变得更容易。

幂等性

最好使用幂等的表达式,因为它没有副作用,并且能提升 Angular 变更检测的性能。

在 Angular 的术语中,幂等的表达式应该总是返回完全相同的东西,直到某个依赖值发生改变。

在单独的一次事件循环中,被依赖的值不应该改变。 如果幂等的表达式返回一个字符串或数字,连续调用它两次,也应该返回相同的字符串或数字。 如果幂等的表达式返回一个对象(包括DateArray),连续调用它两次,也应该返回同一个对象的引用

回到顶部

模板语句

模板语句用来响应由绑定目标(如 HTML 元素、组件或指令)触发的事件。 模板语句将在事件绑定一节看到,它出现在=号右侧的引号中,就像这样:(event)="statement"

<button (click)="deleteHero()">Delete hero</button>

模板语句有副作用。 这是事件处理的关键。因为我们要根据用户的输入更新应用状态。

响应事件是 Angular 中“单向数据流”的另一面。 在一次事件循环中,可以随意改变任何地方的任何东西。

和模板表达式一样,模板语句使用的语言也像 JavaScript。 模板语句解析器和模板表达式解析器有所不同,特别之处在于它支持基本赋值 (=) 和表达式链 (;,)。

然而,某些 JavaScript 语法仍然是不允许的:

语句上下文

和表达式中一样,语句只能引用语句上下文中 —— 通常是正在绑定事件的那个组件实例

典型的语句上下文就是当前组件的实例。 (click)="deleteHero()"中的deleteHero就是这个数据绑定组件上的一个方法。

<button (click)="deleteHero()">Delete hero</button>

语句上下文可以引用模板自身上下文中的属性。 在下面的例子中,就把模板的$event对象、模板输入变量 (let hero)和模板引用变量 (#heroForm)传给了组件中的一个事件处理器方法。

<button (click)="onSave($event)">Save</button>
<button *ngFor="let hero of heroes" (click)="deleteHero(hero)">{{hero.name}}</button>
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>

模板上下文中的变量名的优先级高于组件上下文中的变量名。在上面的deleteHero(hero)中,hero是一个模板输入变量,而不是组件中的hero属性。

模板语句不能引用全局命名空间的任何东西。比如不能引用windowdocument,也不能调用console.logMath.max

语句指南

和表达式一样,避免写复杂的模板语句。 常规是函数调用或者属性赋值。

现在,对模板表达式和语句有了一点感觉了吧。 除插值表达式外,还有各种各样的数据绑定语法,是学习它们是时候了。

回到顶部

绑定语法:概览

数据绑定是一种机制,用来协调用户所见和应用数据。 虽然我们能往 HTML 推送值或者从 HTML 拉取值, 但如果把这些琐事交给数据绑定框架处理, 应用会更容易编写、阅读和维护。 只要简单地在绑定源和目标 HTML 元素之间声明绑定,框架就会完成这项工作。

Angular 提供了各种各样的数据绑定,本章将逐一讨论。 不过我们要先从高层视角来看看 Angular 数据绑定及其语法。

绑定的类型可以根据数据流的方向分成三类: 从数据源到视图从视图到数据源以及双向的从视图到数据源再到视图

数据方向

语法

绑定类型

单向

从数据源

到视图目标

{{expression}}
[target]="expression"
bind-target="expression"

插值表达式

Property

Attribute

样式

单向

从视图目标

到数据源

(target)="statement"
on-target="statement"

事件

双向

[(target)]="expression"
bindon-target="expression"

双向

译注:由于 HTML attribute 和 DOM property 在中文中都被翻译成了“属性”,无法区分, 而接下来的部分重点是对它们进行比较。

我们无法改变历史,因此,在本章的翻译中,保留了它们的英文形式,不加翻译,以免混淆。 本章中,如果提到“属性”的地方,一定是指 property,因为在 Angular 中,实际上很少涉及 attribute。

但在其它章节中,为简单起见,凡是能通过上下文明显区分开的,就仍统一译为“属性”, 区分不明显的,会加注英文。

除了插值表达式之外的绑定类型,在等号左边是目标名, 无论是包在括号中 ([]()) 还是用前缀形式 (bind-on-bindon-) 。

这个目标名就是属性(Property)的名字。它可能看起来像是元素属性(Attribute)的名字,但它不是。 要理解它们的不同点,我们必须尝试用另一种方式来审视模板中的 HTML。

新的思维模型

数据绑定的威力和允许用自定义标记扩展 HTML 词汇的能力,容易误导我们把模板 HTML 当成 HTML+

它其实就是 HTML+。 但它也跟我们熟悉的 HTML 有着显著的不同。 我们需要一种新的思维模型。

在正常的 HTML 开发过程中,我们使用 HTML 元素创建视觉结构, 通过把字符串常量设置到元素的 attribute 来修改那些元素。

<div class="special">Mental Model</div>
<img src="images/hero.png">
<button disabled>Save</button>

在 Angular 模板中,我们仍使用同样的方式来创建结构和初始化 attribute 值。

然后,用封装了 HTML 的组件创建新元素,并把它们当作原生 HTML 元素在模板中使用。

<!-- Normal HTML -->
<div class="special">Mental Model</div>
<!-- Wow! A new element! -->
<hero-detail></hero-detail>

这就是HTML+。

现在开始学习数据绑定。我们碰到的第一种数据绑定是这样的:

<!-- Bind button disabled state to `isUnchanged` property -->
<button [disabled]="isUnchanged">Save</button>

过会儿再认识那个怪异的方括号记法。直觉告诉我们,我们正在绑定按钮的disabled attribute。 并把它设置为组件的isUnchanged属性的当前值。

但我们的直觉是错的!日常的 HTML 思维模式在误导我们。 实际上,一旦开始数据绑定,就不再跟 HTML attribute 打交道了。 这里不是设置 attribute,而是设置 DOM 元素、组件和指令的 property。

HTML attribute 与 DOM property 的对比

要想理解 Angular 绑定如何工作,重点是搞清 HTML attribute 和 DOM property 之间的区别。

attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。

最后一类尤其让人困惑…… 除非我们能理解这个普遍原则:

attribute 初始化 DOM property,然后它们的任务就完成了。property 的值可以改变;attribute 的值不能改变。

例如,当浏览器渲染<input type="text" value="Bob">时,它将创建相应 DOM 节点, 其value property 被初始化为 “Bob”。

当用户在输入框中输入 “Sally” 时,DOM 元素的value property 变成了 “Sally”。 但是这个 HTML value attribute 保持不变。如果我们读取 input 元素的 attribute,就会发现确实没变: input.getAttribute('value') // 返回 "Bob"

HTML attribute value指定了初始值;DOM value property 是当前值。

disabled attribute 是另一个古怪的例子。按钮的disabled propertyfalse,因为默认情况下按钮是可用的。 当我们添加disabled attribute 时,只要它出现了按钮的disabled property 就初始化为true,于是按钮就被禁用了。

添加或删除disabled attribute会禁用或启用这个按钮。但 attribute 的值无关紧要,这就是我们为什么没法通过 <button disabled="false">仍被禁用</button>这种写法来启用按钮。

设置按钮的disabled property(如,通过 Angular 绑定)可以禁用或启用这个按钮。 这就是 property 的价值。

就算名字相同,HTML attribute 和 DOM property 也不是同一样东西。

这句话值得再强调一次:

模板绑定是通过 property事件来工作的,而不是 attribute

没有 attribute 的世界

在 Angular 的世界中,attribute 唯一的作用是用来初始化元素和指令的状态。 当进行数据绑定时,只是在与元素和指令的 property 和事件打交道,而 attribute 就完全靠边站了。

把这个思维模型牢牢的印在脑子里,接下来,学习什么是绑定目标。

绑定目标

数据绑定的目标是 DOM 中的某些东西。 这个目标可能是(元素 | 组件 | 指令的)property、(元素 | 组件 | 指令的)事件,或(极少数情况下) attribute 名。 下面是的汇总表:

绑定类型

目标

范例

Property

元素的 property

组件的 property

指令的 property

<img [src]="heroImageUrl">
<hero-detail [hero]="currentHero"></hero-detail>
<div [ngClass]="{special: isSpecial}"></div>

事件

元素的事件

组件的事件

指令的事件

<button (click)="onSave()">Save</button>
<hero-detail (deleteRequest)="deleteHero()"></hero-detail>
<div (myClick)="clicked=$event" clickable>click me</div>

双向

事件与 property

<input [(ngModel)]="name">
Attribute

attribute(例外情况)

<button [attr.aria-label]="help">help</button>

CSS 类

class property

<div [class.special]="isSpecial">Special</div>

样式

style property

<button [style.color]="isSpecial ? 'red' : 'green'">

放开眼界,我们来看看每种绑定类型的具体情况。

回到顶部

属性绑定 ( [属性名] )

当要把视图元素的属性 (property) 设置为模板表达式时,就要写模板的属性 (property) 绑定

最常用的属性绑定是把元素属性设置为组件属性的值。 下面这个例子中,image 元素的src属性会被绑定到组件的heroImageUrl属性上:

<img [src]="heroImageUrl">

另一个例子是当组件说它isUnchanged(未改变)时禁用按钮:

<button [disabled]="isUnchanged">Cancel is disabled</button>

另一个例子是设置指令的属性:

<div [ngClass]="classes">[ngClass] binding to the classes property</div>

还有另一个例子是设置自定义组件的模型属性(这是父子组件之间通讯的重要途径):

<hero-detail [hero]="currentHero"></hero-detail>

单向输入

人们经常把属性绑定描述成单向数据绑定,因为值的流动是单向的,从组件的数据属性流动到目标元素的属性。

不能使用属性绑定来从目标元素拉取值,也不能绑定到目标元素的属性来读取它。只能设置它。

也不能使用属性 绑定 来调用目标元素上的方法。

如果这个元素触发了事件,可以通过事件绑定来监听它们。

如果必须读取目标元素上的属性或调用它的某个方法,得用另一种技术。 参见 API 参考手册中的 ViewChildContentChild

绑定目标

包裹在方括号中的元素属性名标记着目标属性。下列代码中的目标属性是 image 元素的src属性。

<img [src]="heroImageUrl">

有些人喜欢用bind-前缀的可选形式,并称之为规范形式

<img bind-src="heroImageUrl">

目标的名字总是 property 的名字。即使它看起来和别的名字一样。 看到src时,可能会把它当做 attribute。不!它不是!它是 image 元素的 property 名。

元素属性可能是最常见的绑定目标,但 Angular 会先去看这个名字是否是某个已知指令的属性名,就像下面的例子中一样:

<div [ngClass]="classes">[ngClass] binding to the classes property</div>

严格来说,Angular 正在匹配指令的输入属性的名字。 这个名字是指令的inputs数组中所列的名字,或者是带有@Input()装饰器的属性。 这些输入属性被映射为指令自己的属性。

如果名字没有匹配上已知指令或元素的属性,Angular 就会报告“未知指令”的错误。

消除副作用

正如以前讨论过的,模板表达式的计算不能有可见的副作用。表达式语言本身可以提供一部分安全保障。 不能在属性绑定表达式中对任何东西赋值,也不能使用自增、自减运算符。

当然,表达式可能会调用具有副作用的属性或方法。但 Angular 没法知道这一点,也没法阻止我们。

表达式中可以调用像getFoo()这样的方法。只有我们知道getFoo()干了什么。 如果getFoo()改变了某个东西,恰好又绑定到个这个东西,我们就可能把自己坑了。 Angular 可能显示也可能不显示变化后的值。Angular 还可能检测到变化,并抛出警告型错误。 一般建议是,只绑定数据属性和那些只返回值而不做其它事情的方法。

返回恰当的类型

模板表达式应该返回目标属性所需类型的值。 如果目标属性想要个字符串,就返回字符串。 如果目标属性想要个数字,就返回数字。 如果目标属性想要个对象,就返回对象。

HeroDetail组件的hero属性想要一个Hero对象,那就在属性绑定中精确地给它一个Hero对象:

<hero-detail [hero]="currentHero"></hero-detail>

别忘了方括号

方括号告诉 Angular 要计算模板表达式。 如果忘了加方括号,Angular 会把这个表达式当做字符串常量看待,并用该字符串来初始化目标属性。 它不会计算这个字符串。

不要出现这样的失误:

<!-- ERROR: HeroDetailComponent.hero expects a
     Hero object, not the string "currentHero" -->
  <hero-detail hero="currentHero"></hero-detail>

一次性字符串初始化

当满足下列条件时,应该省略括号:

我们经常这样在标准 HTML 中用这种方式初始化 attribute,这种方式也可以用在初始化指令和组件的属性。 下面这个例子把HeroDetailComponentprefix属性初始化为固定的字符串,而不是模板表达式。Angular 设置它,然后忘记它。

<hero-detail prefix="You are my" [hero]="currentHero"></hero-detail>

作为对比,[hero]绑定是组件的currentHero属性的活绑定,它会一直随着更新。

属性绑定还是插值表达式?

我们通常得在插值表达式和属性绑定之间做出选择。 下列这几对绑定做的事情完全相同:

<p><img src="{{heroImageUrl}}"> is the <i>interpolated</i> image.</p>
<p><img [src]="heroImageUrl"> is the <i>property bound</i> image.</p>

<p><span>"{{title}}" is the <i>interpolated</i> title.</span></p>
<p>"<span [innerHTML]="title"></span>" is the <i>property bound</i> title.</p>

在多数情况下,插值表达式是更方便的备选项。 实际上,在渲染视图之前,Angular 把这些插值表达式翻译成相应的属性绑定。

当要渲染的数据类型是字符串时,没有技术上的理由证明哪种形式更好。 我们倾向于可读性,所以倾向于插值表达式。 建议建立代码风格规则,选择一种形式, 这样,既遵循了规则,又能让手头的任务做起来更自然。

但数据类型不是字符串时,就必须使用属性绑定了。

内容安全

假设下面的恶意内容

evilTitle = 'Template <script>alert("evil never sleeps")</script>Syntax';

幸运的是,Angular 数据绑定对危险 HTML 有防备。 在显示它们之前,它对内容先进行消毒。 不管是插值表达式还是属性绑定,都不会允许带有 script 标签的 HTML 泄漏到浏览器中。

<!--
  Angular generates warnings for these two lines as it sanitizes them
  WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).
 -->
<p><span>"{{evilTitle}}" is the <i>interpolated</i> evil title.</span></p>
<p>"<span [innerHTML]="evilTitle"></span>" is the <i>property bound</i> evil title.</p>

插值表达式处理 script 标签与属性绑定有所不同,但是二者都只渲染没有危害的内容。

evil title made safe

回到顶部

attribute、class 和 style 绑定

模板语法为那些不太适合使用属性绑定的场景提供了专门的单向数据绑定形式。

attribute 绑定

可以通过attribute 绑定来直接设置 attribute 的值。

这是“绑定到目标属性 (property)”这条规则中唯一的例外。这是唯一的能创建和设置 attribute 的绑定形式。

本章中,通篇都在说通过属性绑定来设置元素的属性总是好于用字符串设置 attribute。为什么 Angular 还提供了 attribute 绑定呢?

因为当元素没有属性可绑的时候,就必须使用 attribute 绑定。

考虑 ARIASVG 和 table 中的 colspan/rowspan 等 attribute。 它们是纯粹的 attribute,没有对应的属性可供绑定。

如果想写出类似下面这样的东西,现状会令我们痛苦:

<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>

会得到这个错误:

Template parse errors:
Can't bind to 'colspan' since it isn't a known native property
(模板解析错误:不能绑定到 'colspan',因为它不是已知的原生属性)

正如提示中所说,<td>元素没有colspan属性。 但是插值表达式和属性绑定只能设置属性,不能设置 attribute。

我们需要 attribute 绑定来创建和绑定到这样的 attribute。

attribute 绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而是由attr前缀,一个点 (.) 和 attribute 的名字组成。 可以通过值为字符串的表达式来设置 attribute 的值。

这里把[attr.colspan]绑定到一个计算值:

<table border=1>
  <!--  expression calculates colspan=2 -->
  <tr><td [attr.colspan]="1 + 1">One-Two</td></tr>

  <!-- ERROR: There is no `colspan` property to set!
    <tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
  -->

  <tr><td>Five</td><td>Six</td></tr>
</table>

这里是表格渲染出来的样子:

One-Two
FiveSix

attribute 绑定的主要用例之一是设置 ARIA attribute(译注:ARIA指可访问性,用于给残障人士访问互联网提供便利), 就像这个例子中一样:

<!-- create and set an aria attribute for assistive technology -->
<button [attr.aria-label]="actionName">{{actionName}} with Aria</button>
回到顶部

CSS 类绑定

借助 CSS 类绑定,可以从元素的class attribute 上添加和移除 CSS 类名。

CSS 类绑定绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而是由class前缀,一个点 (.)和 CSS 类的名字组成, 其中后两部分是可选的。形如:[class.class-name]

下列例子示范了如何通过 CSS 类绑定来添加和移除应用的 "special" 类。不用绑定直接设置 attribute 时是这样的:

<!-- standard class attribute setting  -->
<div class="bad curly special">Bad curly special</div>

可以把它改写为绑定到所需 CSS 类名的绑定;这是一个或者全有或者全无的替换型绑定。 (译注:即当 badCurly 有值时 class 这个 attribute 设置的内容会被完全覆盖)

<!-- reset/override all class names with a binding  -->
<div class="bad curly special"
     [class]="badCurly">Bad curly</div>

最后,可以绑定到特定的类名。 当模板表达式的求值结果是真值时,Angular 会添加这个类,反之则移除它。

<!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>

<!-- binding to `class.special` trumps the class attribute -->
<div class="special"
     [class.special]="!isSpecial">This one is not so special</div>

虽然这是切换单一类名的好办法,但我们通常更喜欢使用 NgClass指令 来同时管理多个类名。

回到顶部

样式绑定

通过样式绑定,可以设置内联样式。

样式绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而由style前缀,一个点 (.)和 CSS 样式的属性名组成。 形如:[style.style-property]

<button [style.color]="isSpecial ? 'red': 'green'">Red</button>
<button [style.background-color]="canSave ? 'cyan': 'grey'" >Save</button>

有些样式绑定中的样式带有单位。在这里,以根据条件用 “em” 和 “%” 来设置字体大小的单位。

<button [style.font-size.em]="isSpecial ? 3 : 1" >Big</button>
<button [style.font-size.%]="!isSpecial ? 150 : 50" >Small</button>

虽然这是设置单一样式的好办法,但我们通常更喜欢使用 NgStyle指令 来同时设置多个内联样式。

注意,样式属性命名方法可以用中线命名法,像上面的一样 也可以用驼峰式命名法,如fontSize

回到顶部

事件绑定 ( (事件名) )

前面遇到的绑定的数据流都是单向的:从组件到元素

但用户不会只盯着屏幕看。他们会在输入框中输入文本。他们会从列表中选取条目。 他们会点击按钮。这类用户动作可能导致反向的数据流:从元素到组件

知道用户动作的唯一方式是监听某些事件,如按键、鼠标移动、点击和触摸屏幕。 可以通过 Angular 事件绑定来声明对哪些用户动作感兴趣。

事件绑定语法由等号左侧带圆括号的目标事件和右侧引号中的模板语句组成。 下面事件绑定监听按钮的点击事件。每当点击发生时,都会调用组件的onSave()方法。

<button (click)="onSave()">Save</button>

目标事件

圆括号中的名称 —— 比如(click) —— 标记出目标事件。在下面例子中,目标是按钮的 click 事件。

<button (click)="onSave()">Save</button>

有些人更喜欢带on-前缀的备选形式,称之为规范形式

<button on-click="onSave()">On Save</button>

元素事件可能是更常见的目标,但 Angular 会先看这个名字是否能匹配上已知指令的事件属性,就像下面这个例子:

<!-- `myClick` is an event on the custom `ClickDirective` -->
<div (myClick)="clickMessage=$event" clickable>click with myClick</div>

更多关于该myClick指令的解释,见给输入/输出属性起别名

如果这个名字没能匹配到元素事件或已知指令的输出属性,Angular 就会报“未知指令”错误。

$event 和事件处理语句

在事件绑定中,Angular 会为目标事件设置事件处理器。

当事件发生时,这个处理器会执行模板语句。 典型的模板语句通常涉及到响应事件执行动作的接收器,例如从 HTML 控件中取得值,并存入模型。

绑定会通过名叫$event的事件对象传递关于此事件的信息(包括数据值)。

事件对象的形态取决于目标事件。如果目标事件是原生 DOM 元素事件, $event就是 DOM事件对象,它有像targettarget.value这样的属性。

考虑这个范例:

<input [value]="currentHero.name"
       (input)="currentHero.name=$event.target.value" >

上面的代码在把输入框的value属性绑定到firstName属性。 要监听对值的修改,代码绑定到输入框的input事件。 当用户造成更改时,input事件被触发,并在包含了 DOM 事件对象 ($event) 的上下文中执行这条语句。

要更新firstName属性,就要通过路径$event.target.value来获取更改后的值。

如果事件属于指令(回想一下,组件是指令的一种),那么$event具体是什么由指令决定。

使用 EventEmitter 实现自定义事件

通常,指令使用 Angular EventEmitter 来触发自定义事件。 指令创建一个EventEmitter实例,并且把它作为属性暴露出来。 指令调用EventEmitter.emit(payload)来触发事件,可以传入任何东西作为消息载荷。 父指令通过绑定到这个属性来监听事件,并通过$event对象来访问载荷。

假设HeroDetailComponent用于显示英雄的信息,并响应用户的动作。 虽然HeroDetailComponent包含删除按钮,但它自己并不知道该如何删除这个英雄。 最好的做法是触发事件来报告“删除用户”的请求。

下面的代码节选自HeroDetailComponent

src/app/hero-detail.component.ts (template)

template: `
<div>
  <img src="{{heroImageUrl}}">
  <span [style.text-decoration]="lineThrough">
    {{prefix}} {{hero?.name}}
  </span>
  <button (click)="delete()">Delete</button>
</div>`

src/app/hero-detail.component.ts (deleteRequest)

// This component make a request but it can't actually delete a hero.
deleteRequest = new EventEmitter<Hero>();

delete() {
  this.deleteRequest.emit(this.hero);
}

组件定义了deleteRequest属性,它是EventEmitter实例。 当用户点击删除时,组件会调用delete()方法,让EventEmitter发出一个Hero对象。

现在,假设有个宿主的父组件,它绑定了HeroDetailComponentdeleteRequest事件。

<hero-detail (deleteRequest)="deleteHero($event)" [hero]="currentHero"></hero-detail>

deleteRequest事件触发时,Angular 调用父组件的deleteHero方法, 在$event变量中传入要删除的英雄(来自HeroDetail)。

模板语句有副作用

deleteHero方法有副作用:它删除了一个英雄。 模板语句的副作用不仅没问题,反而正是所期望的。

删除这个英雄会更新模型,还可能触发其它修改,包括向远端服务器的查询和保存。 这些变更通过系统进行扩散,并最终显示到当前以及其它视图中。

回到顶部

双向数据绑定 ( [(...)] )

我们经常需要显示数据属性,并在用户作出更改时更新该属性。

在元素层面上,既要设置元素属性,又要监听元素事件变化。

Angular 为此提供一种特殊的双向数据绑定语法:[(x)][(x)]语法结合了属性绑定的方括号[x]事件绑定的圆括号(x)

[( )] = 盒子里的香蕉

想象盒子里的香蕉来记住方括号套圆括号。

当一个元素拥有可以设置的属性x和对应的事件xChange时,解释[(x)]语法就容易多了。 下面的SizerComponent符合这个模式。它有size属性和伴随的sizeChange事件:

src/app/sizer.component.ts

  1. import { Component, EventEmitter, Input, Output } from '@angular/core';
  2. @Component({
  3. selector: 'my-sizer',
  4. template: `
  5. <div>
  6. <button (click)="dec()" title="smaller">-</button>
  7. <button (click)="inc()" title="bigger">+</button>
  8. <label [style.font-size.px]="size">FontSize: {{size}}px</label>
  9. </div>`
  10. })
  11. export class SizerComponent {
  12. @Input() size: number | string;
  13. @Output() sizeChange = new EventEmitter<number>();
  14. dec() { this.resize(-1); }
  15. inc() { this.resize(+1); }
  16. resize(delta: number) {
  17. this.size = Math.min(40, Math.max(8, +this.size + delta));
  18. this.sizeChange.emit(this.size);
  19. }
  20. }

size的初始值是一个输入值,来自属性绑定。(译注:注意size前面的@Input) 点击按钮,在最小/最大值范围限制内增加或者减少size。 然后用调整后的size触发sizeChange事件。

下面的例子中,AppComponent.fontSize被双向绑定到SizerComponent

<my-sizer [(size)]="fontSizePx"></my-sizer>
<div [style.font-size.px]="fontSizePx">Resizable Text</div>

SizerComponent.size初始值是AppComponent.fontSizePx。 点击按钮时,通过双向绑定更新AppComponent.fontSizePx。 被修改的AppComponent.fontSizePx通过样式绑定,改变文本的显示大小。

双向绑定语法实际上是属性绑定和事件绑定的语法糖。 Angular将SizerComponent的绑定分解成这样:

<my-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></my-sizer>

$event变量包含了SizerComponent.sizeChange事件的荷载。 当用户点击按钮时,Angular 将$event赋值给AppComponent.fontSizePx

显然,比起单独绑定属性和事件,双向数据绑定语法显得非常方便。

我们希望能在像<input><select>这样的 HTML 元素上使用双向数据绑定。 可惜,原生 HTML 元素不遵循x值和xChange事件的模式。

幸运的是,Angular 以 NgModel 指令为桥梁,允许在表单元素上使用双向数据绑定。

回到顶部

内置指令

上一版本的 Angular 中包含了超过 70 个内置指令。 社区贡献了更多,这还没算为内部应用而创建的无数私有指令。

在新版的 Angular 中不需要那么多指令。 使用更强大、更富有表现力的 Angular 绑定系统,其实可以达到同样的效果。 如果能用简单的绑定达到目的,为什么还要创建指令来处理点击事件呢?

<button (click)="onSave()">Save</button>

我们仍然可以从简化复杂任务的指令中获益。 Angular 发布时仍然带有内置指令,只是没那么多了。 我们仍会写自己的指令,只是没那么多了。

下面来看一下那些最常用的内置指令。它们可分为属性型指令结构型指令

内置属性型指令

属性型指令会监听和修改其它HTML元素或组件的行为、元素属性(Attribute)、DOM属性(Property)。 它们通常会作为HTML属性的名称而应用在元素上。

更多的细节参见属性型指令一章。 很多Angular模块,比如RouterModuleFormsModule都定义了自己的属性型指令。 本节将会介绍几个最常用的属性型指令:

回到顶部

NgClass

我们经常用动态添加或删除 CSS 类的方式来控制元素如何显示。 通过绑定到NgClass,可以同时添加或移除多个类。

CSS 类绑定 是添加或删除单个类的最佳途径。

<!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>

当想要同时添加或移除多个 CSS 类时,NgClass指令可能是更好的选择。

试试把ngClass绑定到一个 key:value 形式的控制对象。这个对象中的每个 key 都是一个 CSS 类名,如果它的 value 是true,这个类就会被加上,否则就会被移除。

组件方法setCurrentClasses可以把组件的属性currentClasses设置为一个对象,它将会根据三个其它组件的状态为truefalse而添加或移除三个类。

currentClasses: {};
setCurrentClasses() {
  // CSS classes: added/removed per current state of component properties
  this.currentClasses =  {
    saveable: this.canSave,
    modified: !this.isUnchanged,
    special:  this.isSpecial
  };
}

NgClass属性绑定到currentClasses,根据它来设置此元素的CSS类:

<div [ngClass]="currentClasses">This div is initially saveable, unchanged, and special</div>

你既可以在初始化时调用setCurrentClassess(),也可以在所依赖的属性变化时调用。

回到顶部

NgStyle

我们可以根据组件的状态动态设置内联样式。 NgStyle绑定可以同时设置多个内联样式。

样式绑定是设置单一样式值的简单方式。

<div [style.font-size]="isSpecial ? 'x-large' : 'smaller'" >
  This div is x-large or smaller.
</div>

如果要同时设置多个内联样式,NgStyle指令可能是更好的选择。

NgStyle需要绑定到一个 key:value 控制对象。 对象的每个 key 是样式名,它的 value 是能用于这个样式的任何值。

来看看组件的setCurrentStyles方法,它会根据另外三个属性的状态把组件的currentStyles属性设置为一个定义了三个样式的对象:

currentStyles: {};
setCurrentStyles() {
  // CSS styles: set per current state of component properties
  this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };
}

NgStyle属性绑定到currentStyles,以据此设置此元素的样式:

<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>

你既可以在初始化时调用setCurrentStyles(),也可以在所依赖的属性变化时调用。

回到顶部

NgModel - 使用[(ngModel)]双向绑定到表单元素

当开发数据输入表单时,我们通常都要既显示数据属性又根据用户的更改去修改那个属性。

使用NgModel指令进行双向数据绑定可以简化这种工作。例子如下:

<input [(ngModel)]="currentHero.name">

使用 ngModel 时需要 FormsModule

在使用ngModel指令进行双向数据绑定之前,我们必须导入FormsModule并把它添加到Angular模块的imports列表中。 要了解FormsModulengModel的更多知识,参见表单一章。

导入FormsModule并让[(ngModel)]可用的代码如下:

src/app/app.module.ts (FormsModule import)

import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // <--- JavaScript import from Angular

/* Other imports */

@NgModule({
  imports: [
    BrowserModule,
    FormsModule  // <--- import into the NgModule
  ],
  /* Other module metadata */
})
export class AppModule { }

[(ngModel)]内幕

回头看看name绑定,注意,你可以通过分别绑定到<input>元素的value属性和input事件来达到同样的效果。

<input [value]="currentHero.name"
       (input)="currentHero.name=$event.target.value" >

那样显得很笨重,谁会记得该设置哪个元素属性以及当用户修改时触发哪个事件? 你该如何提取输入框中的文本并且更新数据属性?谁会希望每次都去查资料来确定这些?

ngModel指令通过自己的输入属性ngModel和输出属性ngModelChange隐藏了那些细节。

<input
  [ngModel]="currentHero.name"
  (ngModelChange)="currentHero.name=$event">

ngModel输入属性会设置该元素的值,并通过ngModelChange的输出属性来监听元素值的变化。

各种元素都有很多特有的处理细节,因此NgModel指令只支持实现了ControlValueAccessor的元素, 它们能让元素适配本协议。 <input>输入框正是其中之一。 Angular为所有的基础HTML表单都提供了值访问器(Value accessor)表单一章展示了如何绑定它们。

我们不能把[(ngModel)]用到非表单类的原生元素或第三方自定义组件上,除非写一个合适的值访问器,这种技巧超出了本章的范围。

我们自己写的Angular组件不需要值访问器,因为我们可以让值和事件的属性名适应Angular基本的双向绑定语法,而不使用NgModel前面看过的sizer就是使用这种技巧的例子。

使用独立的ngModel绑定优于绑定到该元素的原生属性,那样我们可以做得更好。

我们不用被迫两次引用这个数据属性,Angular可以捕获该元素的数据属性,并且通过一个简单的声明来设置它,这样它就可以使用[(ngModel)]语法了。

<input [(ngModel)]="currentHero.name">

[(ngModel)]就是你需要的一切吗?有没有什么理由回退到它的展开形式?

[(ngModel)]语法只能设置数据绑定属性。 如果要做更多或者做点不一样的事,也可以写它的展开形式。

下面这个生造的例子强制输入框的内容变成大写:

<input
  [ngModel]="currentHero.name"
  (ngModelChange)="setUppercaseName($event)">

这里是所有这些变体的动画,包括这个大写转换的版本:

NgModel variations

回到顶部

内置结构型指令

结构型指令的职责是HTML布局。 它们塑造或重塑DOM的结构,这通常是通过添加、移除和操纵它们所附加到的宿主元素来实现的。

关于结构型指令的详情参见结构型指令一章,在那里我们将学到:

本节是对常见结构型指令的简介:

NgIf

通过把NgIf指令应用到元素上(称为宿主元素),我们可以往DOM中添加或从DOM中移除这个元素。 在下面的例子中,该指令绑定到了类似于isActive这样的条件表达式。

<hero-detail *ngIf="isActive"></hero-detail>

别忘了ngIf前面的星号(*)。

isActive表达式返回真值时,NgIfHeroDetailComponent添加到DOM中;为假时,NgIf会从DOM中移除HeroDetailComponent,并销毁该组件及其所有子组件。

这和显示/隐藏不是一回事

我们也可以通过类绑定样式绑定来显示或隐藏一个元素。

<!-- isSpecial is true -->
<div [class.hidden]="!isSpecial">Show with class</div>
<div [class.hidden]="isSpecial">Hide with class</div>

<!-- HeroDetail is in the DOM but hidden -->
<hero-detail [class.hidden]="isSpecial"></hero-detail>

<div [style.display]="isSpecial ? 'block' : 'none'">Show with style</div>
<div [style.display]="isSpecial ? 'none'  : 'block'">Hide with style</div>

但隐藏子树和用NgIf排除子树是截然不同的。

当隐藏子树时,它仍然留在 DOM 中。 子树中的组件及其状态仍然保留着。 即使对于不可见属性,Angular 也会继续检查变更。 子树可能占用相当可观的内存和运算资源。

NgIffalse时,Angular 从 DOM 中物理地移除了这个元素子树。 它销毁了子树中的组件及其状态,也潜在释放了可观的资源,最终让用户体验到更好的性能。

显示/隐藏的技术对于只有少量子元素的元素是很好用的,但要当心别试图隐藏大型组件树。相比之下,NgIf则是个更安全的选择。

防范空指针错误

ngIf指令通常会用来防范空指针错误。 而显示/隐藏的方式是无法防范的,当一个表达式尝试访问空值的属性时,Angular就会抛出一个异常。

这里我们用NgIf来保护了两个<div>防范空指针错误。 currentHero的名字只有当存在currentHero时才会显示出来。 而nullHero永远不会显示。

<div *ngIf="currentHero">Hello, {{currentHero.name}}</div>
<div *ngIf="nullHero">Hello, {{nullHero.name}}</div>

参见稍后的安全导航操作符部分。

回到顶部

NgFor

NgFor是一个重复器指令 —— 自定义数据显示的一种方式。 我们的目标是展示一个由多个条目组成的列表。首先定义了一个 HTML 块,它规定了单个条目应该如何显示。 再告诉 Angular 把这个块当做模板,渲染列表中的每个条目。

下例中,NgFor应用在一个简单的<div>上:

<div *ngFor="let hero of heroes">{{hero.name}}</div>

也可以把NgFor应用在一个组件元素上,就下例这样:

<hero-detail *ngFor="let hero of heroes" [hero]="hero"></hero-detail>

不要忘了ngFor前面的星号 (*)。

赋值给*ngFor的文本是用于指导重复器如何工作的指令。

NgFor 微语法

赋值给*ngFor的字符串不是模板表达式。 它是一个微语法 —— 由 Angular 自己解释的小型语言。在这个例子中,字符串"let hero of heroes"的含义是:

取出heroes数组中的每个英雄,把它存入局部变量hero中,并在每次迭代时对模板 HTML 可用

Angular 把这个指令翻译成了一个<ng-template>包裹的宿主元素,然后使用这个模板重复创建出一组新元素,并且绑定到列表中的每一个hero

要了解微语法的更多知识,参见结构型指令一章。

模板输入变量

hero前的let关键字创建了一个名叫hero模板输入变量ngFor指令在由父组件的heroes属性返回的heroes数组上迭代,每次迭代都从数组中把当前元素赋值给hero变量。

我们可以在ngFor的宿主元素(及其子元素)中引用模板输入变量hero,从而访问该英雄的属性。 这里它首先在一个插值表达式中被引用到,然后通过一个绑定把它传给了<hero-detail>组件的hero属性。

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<hero-detail *ngFor="let hero of heroes" [hero]="hero"></hero-detail>

要了解更多模板输入变量的知识,参见结构型指令一章。

带索引的*ngFor

NgFor指令上下文中的index属性返回一个从零开始的索引,表示当前条目在迭代中的顺序。 我们可以通过模板输入变量捕获这个index值,并把它用在模板中。

下面这个例子把index捕获到了i变量中,并且把它显示在英雄名字的前面。

<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>

要学习更多的类似 index 的值,例如lastevenodd,请参阅 NgFor API 参考

trackBy*ngFor

ngFor指令有时候会性能较差,特别是在大型列表中。 对一个条目的一丁点改动、移除或添加,都会导致级联的 DOM 操作。

例如,我们可以通过重新从服务器查询来刷新英雄列表。 刷新后的列表可能包含很多(如果不是全部的话)以前显示过的英雄。

它们中的绝大多数(如果不是所有的话)都是以前显示过的英雄。我们知道这一点,是因为每个英雄的id没有变化。 但在 Angular 看来,它只是一个由新的对象引用构成的新列表, 它没有选择,只能清理旧列表、舍弃那些 DOM 元素,并且用新的 DOM 元素来重建一个新列表。

如果给它指定一个trackBy,Angular 就可以避免这种折腾。 我们往组件中添加一个方法,它会返回NgFor应该追踪的值。 在这里,这个值就是英雄的id

trackByHeroes(index: number, hero: Hero): number { return hero.id; }

在微语法中,把trackBy设置为该方法。

<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
  ({{hero.id}}) {{hero.name}}
</div>

这里展示了trackBy的效果。 "Reset heroes"会创建一个具有相同hero.id的新英雄。 "Change ids"则会创建一个具有新hero.id的新英雄。

trackBy

回到顶部

NgSwitch指令

NgSwitch指令类似于JavaScript的switch语句。 它可以从多个可能的元素中根据switch条件来显示某一个。 Angular只会把选中的元素放进DOM中。

NgSwitch实际上包括三个相互协作的指令:NgSwitchNgSwitchCaseNgSwitchDefault,例子如下:

<div [ngSwitch]="currentHero.emotion">
  <happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero"></happy-hero>
  <sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero"></sad-hero>
  <confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></confused-hero>
  <unknown-hero  *ngSwitchDefault           [hero]="currentHero"></unknown-hero>
</div>
trackBy

NgSwitch是主控指令,要把它绑定到一个返回候选值的表达式。 本例子中的emotion是个字符串,但实际上这个候选值可以是任意类型。

绑定到[ngSwitch]。如果试图用*ngSwitch的形式使用它就会报错,这是因为NgSwitch是一个属性型指令,而不是结构型指令。 它要修改的是所在元素的行为,而不会直接接触DOM结构。

绑定到*ngSwitchCase*ngSwitchDefault NgSwitchCaseNgSwitchDefault 指令都是结构型指令,因为它们会从DOM中添加或移除元素。

这组指令在要添加或移除组件元素时会非常有用。 这个例子会在hero-switch.components.ts中定义的四个“感人英雄”组件之间选择。 每个组件都有一个输入属性hero,它绑定到父组件的currentHero上。

这组指令在原生元素和Web Component上都可以正常工作。 比如,你可以把<confused-hero>分支改成这样:

<div *ngSwitchCase="'confused'">Are you as confused as {{currentHero.name}}?</div>
回到顶部

模板引用变量 ( #var )

模板引用变量通常用来引用模板中的某个DOM元素,它还可以引用Angular组件或指令或Web Component

使用井号 (#) 来声明引用变量。 #phone的意思就是声明一个名叫phone的变量来引用<input>元素。

<input #phone placeholder="phone number">

我们可以在模板中的任何地方引用模板引用变量。 比如声明在<input>上的phone变量就是在模板另一侧的<button>上使用的。

<input #phone placeholder="phone number">

<!-- lots of other elements -->

<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>

模板引用变量怎么得到它的值?

大多数情况下,Angular会把模板引用变量的值设置为声明它的那个元素。 在上一个例子中,phone引用的是表示电话号码<input>框。 "拨号"按钮的点击事件处理器把这个input值传给了组件的callPhone方法。 不过,指令也可以修改这种行为,让这个值引用到别处,比如它自身。 NgForm指令就是这么做的。

下面是表单一章中表单范例的简化版

<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
  <div class="form-group">
    <label for="name">Name
      <input class="form-control" name="name" required [(ngModel)]="hero.name">
    </label>
  </div>
  <button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>
<div [hidden]="!heroForm.form.valid">
  {{submitMessage}}
</div>

模板引用变量heroForm在这个例子中出现了三次,中间隔着一大堆HTML。 heroForm的值是什么?

如果你没有导入过FormsModule,Angular就不会控制这个表单,那么它就是一个HTMLFormElement实例。 这里的heroForm实际上是一个Angular NgForm 指令的引用, 因此具备了跟踪表单中的每个控件的值和有效性的能力。

原生的<form>元素没有form属性,但NgForm指令有。这就解释了为何当heroForm.form.valid是无效时我们可以禁用提交按钮, 并能把整个表单控件树传给父组件的onSubmit方法。

关于模板引用变量的提醒

模板引用变量 (#phone) 和*ngFor部分看到过的模板输入变量 (let phone) 是不同的。 要了解详情,参见结构型指令一章。

模板引用变量的作用范围是整个模板。 不要在同一个模板中多次定义同一个变量名,否则它在运行期间的值是无法确定的。

我们也可以用ref-前缀代替#。 下面的例子中就用把fax变量声明成了ref-fax而不是#fax

<input ref-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>
回到顶部

输入输出属性 ( @Input@Output )

迄今为止,我们主要聚焦在绑定声明的右侧,学习如何在模板表达式和模板语句中绑定到组件成员。 当成员出现在这个位置上,则称之为数据绑定的

本节则专注于绑定到的目标,它位于绑定声明中的左侧。 这些指令的属性必须被声明成输入输出

记住:所有组件皆为指令

我们要重点突出下绑定目标和绑定的区别。

绑定的目标是在=左侧的部分, 则是在=右侧的部分。

绑定的目标是绑定符:[]()[()]中的属性或事件名, 则是引号 (" ") 中的部分或插值符号 ({{}}) 中的部分。

指令中的每个成员都会自动在绑定中可用。 不需要特别做什么,就能在模板表达式或语句中访问指令的成员。

访问目标指令中的成员则受到限制。 只能绑定到那些显式标记为输入输出的属性。

在下面的例子中,iconUrlonSave是组件的成员,它们在=右侧引号语法中被引用了。

<img [src]="iconUrl"/>
<button (click)="onSave()">Save</button>

它们既不是组件的输入也不是输出。它们是绑定的数据源。

现在,看看HeroDetailComponent中的另一个片段,等号(=左侧的是绑定的目标

<hero-detail [hero]="currentHero" (deleteRequest)="deleteHero($event)">
</hero-detail>

HeroDetailComponent.heroHeroDetailComponent.deleteRequest都在绑定声明的左侧HeroDetailComponent.hero在方括号中,它是属性绑定的目标。 HeroDetailComponent.deleteRequest在圆括号中,它是事件绑定的目标。

声明输入和输出属性

目标属性必须被显式的标记为输入或输出。

HeroDetailComponent内部,这些属性被装饰器标记成了输入和输出属性。

@Input()  hero: Hero;
@Output() deleteRequest = new EventEmitter<Hero>();

另外,还可以在指令元数据的inputsoutputs数组中标记出这些成员。比如这个例子:

@Component({
  inputs: ['hero'],
  outputs: ['deleteRequest'],
})

既可以通过装饰器,也可以通过元数据数组来指定输入/输出属性。但别同时用!

输入还是输出?

输入属性通常接收数据值。 输出属性暴露事件生产者,如EventEmitter对象。

输入输出这两个词是从目标指令的角度来说的。

Inputs and outputs

HeroDetailComponent角度来看,HeroDetailComponent.hero是个输入属性, 因为数据流从模板绑定表达式流那个属性。

HeroDetailComponent角度来看,HeroDetailComponent.deleteRequest是个输出属性, 因为事件从那个属性流,流向模板绑定语句中的处理器。

给输入/输出属性起别名

有时需要让输入/输出属性的公开名字不同于内部名字。

这是使用 attribute 指令时的常见情况。 指令的使用者期望绑定到指令名。例如,在<div>上用myClick选择器应用指令时, 希望绑定的事件属性也叫myClick

<div (myClick)="clickMessage=$event" clickable>click with myClick</div>

然而,在指令类中,直接用指令名作为自己的属性名通常都不是好的选择。 指令名很少能描述这个属性是干嘛的。 myClick这个指令名对于用来发出 click 消息的属性就算不上一个好名字。

幸运的是,可以使用约定俗成的公开名字,同时在内部使用不同的名字。 在上面例子中,实际上是把myClick这个别名指向了指令自己的clicks属性。

把别名传进@Input/@Output装饰器,就可以为属性指定别名,就像这样:

@Output('myClick') clicks = new EventEmitter<string>(); //  @Output(alias) propertyName = ...

也可在inputsoutputs数组中为属性指定别名。 可以写一个冒号 (:) 分隔的字符串,左侧是指令中的属性名,右侧则是公开的别名。

@Directive({
  outputs: ['clicks:myClick']  // propertyName:alias
})
回到顶部

模板表达式操作符

模板表达式语言使用了 JavaScript 语法的子集,并补充了几个用于特定场景的特殊操作符。 下面介绍其中的两个:管道安全导航操作符

管道操作符 ( | )

在绑定之前,表达式的结果可能需要一些转换。例如,可能希望把数字显示成金额、强制文本变成大写,或者过滤列表以及进行排序。

Angular 管道对像这样的小型转换来说是个明智的选择。 管道是一个简单的函数,它接受一个输入值,并返回转换结果。 它们很容易用于模板表达式中,只要使用管道操作符 (|) 就行了。

<div>Title through uppercase pipe: {{title | uppercase}}</div>

管道操作符会把它左侧的表达式结果传给它右侧的管道函数。

还可以通过多个管道串联表达式:

<!-- Pipe chaining: convert title to uppercase, then to lowercase -->
<div>
  Title through a pipe chain:
  {{title | uppercase | lowercase}}
</div>

还能对它们使用参数:

<!-- pipe with configuration argument => "February 25, 1970" -->
<div>Birthdate: {{currentHero?.birthdate | date:'longDate'}}</div>

json管道对调试绑定特别有用:

<div>{{currentHero | json}}</div>

它生成的输出是这样的:

{ "id": 0, "name": "Hercules", "emotion": "happy",
  "birthdate": "1970-02-25T08:00:00.000Z",
  "url": "http://www.imdb.com/title/tt0065832/",
  "rate": 325 }
回到顶部

安全导航操作符 ( ?. ) 和空属性路径

Angular 的安全导航操作符 (?.) 是一种流畅而便利的方式,用来保护出现在属性路径中 null 和 undefined 值。 下例中,当currentHero为空时,保护视图渲染器,让它免于失败。

The current hero's name is {{currentHero?.name}}

如果下列数据绑定中title属性为空,会发生什么?

The title is {{title}}

这个视图仍然被渲染出来,但是显示的值是空;只能看到 “The title is”,它后面却没有任何东西。 这是合理的行为。至少应用没有崩溃。

假设模板表达式涉及属性路径,在下例中,显示一个空 (null) 英雄的firstName

The null hero's name is {{nullHero.name}}

JavaScript 抛出了空引用错误,Angular 也是如此:

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

晕,整个视图都不见了

如果确信hero属性永远不可能为空,可以声称这是合理的行为。 如果它必须不能为空,但它仍然是空值,实际上是制造了一个编程错误,它应该被捕获和修复。 这种情况应该抛出异常。

另一方面,属性路径中的空值可能会时常发生,特别是当我们知道数据最终会出现。

当等待数据的时候,视图渲染器不应该抱怨,而应该把这个空属性路径显示为空白,就像上面title属性那样。

不幸的是,当currentHero为空的时候,应用崩溃了。

可以通过用NgIf代码环绕它来解决这个问题。

<!--No hero, div not displayed, no error -->
<div *ngIf="nullHero">The null hero's name is {{nullHero.name}}</div>

或者可以尝试通过&&来把属性路径的各部分串起来,让它在遇到第一个空值的时候,就返回空。

The null hero's name is {{nullHero && nullHero.name}}

这些方法都有价值,但是会显得笨重,特别是当这个属性路径非常长的时候。 想象一下在一个很长的属性路径(如a.b.c.d)中对空值提供保护。

Angular 安全导航操作符 (?.) 是在属性路径中保护空值的更加流畅、便利的方式。 表达式会在它遇到第一个空值的时候跳出。 显示是空的,但应用正常工作,而没有发生错误。

<!-- No hero, no problem! -->
The null hero's name is {{nullHero?.name}}

在像a?.b?.c?.d这样的长属性路径中,它工作得很完美。

回到顶部

小结

我们完成了模板语法的概述。现在,该把如何写组件和指令的知识投入到实际工作当中了。

下一步

速查表