学习资料来自 Angular.cn 与 Angular.io。
模板语法
在线例子
在 Angular 中,组件扮演着控制器或视图模型的角色,模板则扮演视图的角色。
模板中的 HTML
HTML 是 Angular 模板的语言。
Hello Angular
为防范脚本注入攻击的风险, 元素被禁用了(忽略了)。更多内容参见安全。
、
和
元素在此没有使用的意义。
插值表达式 (Interpolation) {{...}}
插值表达式可以把计算后的字符串插入到 HTML 元素标签内的文本或对标签的属性进行赋值。
一般来说,括号间的素材是一个模板表达式,Angular 先对它求值,再把它转换成字符串。
The sum of 1 + 1 is {{1 + 1}}
模板表达式 (Template expressions)
模板表达式产生一个值。Angular 执行这个表达式,并把它赋值给绑定目标的属性,这个绑定目标可能是 HTML 元素、组件或指令。
模板表达式不支持的内容包括:
- 赋值 (
=
,+=
,-=
, ...) -
new
运算符 - 使用
;
或,
的链式表达式 - 自增或自减操作符 (
++
和--
) - 不支持位运算
|
和&
- 具有新的模板表达式运算符,比如
|
和?.
- 不能引用全局命名空间中的任何东西。 不能引用
window
或document
。不能调用console.log
或Math.max
。
表达式上下文 (Expression context)
典型的表达式上下文是组件实例。
The expression context is typically the component instance.
下例中 title
和 isUnchanged
均为 AppComponent 的属性。
{{title}}
changed
表达式的上下文可以包括组件之外的对象。 比如模板输入变量 (let hero)和模板引用变量(#heroInput)就是备选的上下文对象之一。
{{hero.name}}
{{heroInput.value}}
用术语来说,表达式上下文由模版变量,指令上下文对象(如果存在)和组件成员混合而成。如果所引用的名称在上述命名空间中有冲突,那么将会按照模板变量,指令上下文和组件成员的顺序优先选取。
上例展示了一个命名冲突。组件有一个 hero
属性,*ngFor
定义了一个 hero
模版变量。{{hero.name}}
中的 hero
指的是模板变量,而非组件属性。
模板表达式被局限于只能访问来自表达式上下文中的成员。
表达式指南 (Expression guidelines)
模板表达式能成就或毁掉一个应用。应遵循原则:
1. 没有可见的副作用
模板表达式除了目标属性的值以外,不应该改变应用的任何状态。这条规则是 Angular “单向数据流”策略的基础。在一次单独的渲染过程中,视图应该总是稳定的。
2. 执行迅速
Angular 执行模板表达式比我们想象的频繁。当计算代价较高时,应该考虑缓存那些从其它值计算得出的值。
TODO: 如何缓存?
3. 非常简单
模板表达式应保持简单,不要过于复杂。应在组件中实现应用和业务逻辑,使开发和测试变得更容易。
4. 幂等性
使用幂等的表达式没有副作用,并且能提升 Angular 变更检测的性能。
在 Angular 的术语中,幂等的表达式应该总是返回完全相同的东西,直到某个依赖值发生改变。
模板语句 (Template statements)
模板语句用来响应由绑定目标(如 HTML 元素、组件或指令)触发的事件。
A template statement responds to an event raised by a binding target such as an element, component, or directive.
语法:(event)="statement"
模板语句有副作用。这是事件的关键所在,可以根据用户的活动更新应用状态。
A template statement has a side effect. That's the whole point of an event. It's how you update application state from user action.
模板语句解析器 (template statement parser) 和模板表达式解析器 (template expression parser) 有所不同,特别之处在于它支持基本赋值 (=
) 和表达式链 ( ;
和 ,
)。
某些 JavaScript 语法仍然是不允许的:
-
new
运算符 - 自增和自减运算符:
++
和--
- 操作并赋值,例如
+=
和-=
- 位操作符
|
和&
- 模板表达式运算符
语句上下文 (Statement context)
和表达式中一样,语句只能引用语句上下文中——通常是正在绑定事件的那个组件实例。
语句上下文可以引用模板自身上下文中的属性。下例把模板的 $event 对象、模板输入变量(let hero)和模板引用变量(#heroForm)传给了组件中的一个事件处理器方法。
模板语句不能引用全局命名空间的任何东西。比如不能引用 window
或 document
,也不能调用 console.log
或 Math.max
。
语句指南 (Expression guidelines)
和表达式一样,避免写复杂的模板语句。常规是函数调用或者属性赋值。
绑定语法:概览
绑定的类型可以根据数据流的方向分成三类:
- 从数据源到视图(source-to-view)
- 从视图到数据源(view-to-source)
- 双向的从视图到数据源再到视图(view-to-source-to-view)。
单向 source-to-view
语法 | 绑定类型 |
---|---|
{{expression}} |
插值表达式 |
[target]="expression" / bind-target="expression" |
Property, Attribute 类样式 |
单向 view-to-source
语法 | 绑定类型 |
---|---|
(target)="statement" / on-target="statement" |
事件 |
双向
语法 | 绑定类型 |
---|---|
[(target)]="expression" / bindon-target="expression" |
双向 |
绑定类型(插值表达式除外)有一个目标名,它位于等号左边,它是 Property 的名字(注意它不是 Attribute 的名字)。
新的思维模型
HTML attribute 与 DOM property 的对比
attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。
- 少量 HTML attribute 和 property 之间有着 1:1 的映射,如 id。
- 有些 HTML attribute 没有对应的 property,如 colspan。
- 有些 DOM property 没有对应的 attribute,如 textContent。
一旦开始数据绑定,就不再跟 HTML attribute 打交道了。这里不是设置 attribute,而是设置 DOM 元素、组件和指令的 property。
attribute 初始化 DOM property,然后它们的任务就完成了。property 的值可以改变,是当前值;attribute 的值不能改变,是初始值。
即使名字相同,HTML attribute 和 DOM property 也不是同一样东西。
模板绑定是通过 property 和事件来工作的,而不是 attribute。在 Angular 的世界中,attribute 唯一的作用是用来初始化元素和指令的状态。 当进行数据绑定时,只是在与元素和指令的 property 和事件打交道。
绑定目标 (Binding targets)
数据绑定的目标是 DOM 中的某些东西。
Property 绑定类型
目标 | 范例 |
---|---|
Element property |
|
Component property |
|
Directive property |
|
事件绑定类型
目标 | 范例 |
---|---|
Element event |
|
Component event |
|
Directive event |
|
双向绑定类型
目标 | 范例 |
---|---|
Event and property |
|
Attribute 绑定类型
目标 | 范例 |
---|---|
Attribute(例外情况) |
|
CSS 类绑定类型
目标 | 范例 |
---|---|
class property |
|
样式绑定类型
目标 | 范例 |
---|---|
style property |
|
属性绑定 (Property binding) [property]
当要把视图元素的属性 (property) 设置为模板表达式时,就要写模板的属性 (property) 绑定。
最常用的属性绑定是把元素属性设置为组件属性的值。
上例说明:
image
元素的src
属性会被绑定到组件的heroImageUrl
属性上。
其他示例:
[ngClass] binding to the classes property
单向输入 (One-way in)
人们经常把属性绑定描述成单向数据绑定,因为值的流动是单向的,从组件的数据属性流动到目标元素的属性。
注意:
- 不能使用属性绑定来从目标元素拉取值
- 不能绑定到目标元素的属性来读取它。只能设置它。
- 也不能使用属性 绑定 来调用目标元素上的方法。
如果必须读取目标元素上的属性或调用它的某个方法, 参见 API 参考手册中的 ViewChild 和 ContentChild。
绑定目标
两种写法:
![](heroImageUrl)
bind-
前缀的写法被称为规范形式 (canonical form)。
元素属性可能是最常见的绑定目标,但 Angular 会先去看这个名字是否是某个已知指令的属性名。
[ngClass] binding to the classes property
严格来说,Angular 正在匹配指令的输入属性的名字。 这个名字是指令的 inputs
数组中所列的名字,或者是带有 @Input()
装饰器的属性。 这些输入属性被映射为指令自己的属性。
Technically, Angular is matching the name to a directive input
, one of the property names listed in the directive's inputs
array or a property decorated with @Input()
. Such inputs map to the directive's own properties.
如果名字没有匹配上已知指令或元素的属性,Angular 就会报告“未知指令”的错误。
消除副作用
一般建议是,只绑定数据属性和那些只返回值而不做其它事情的方法。
返回恰当的类型
模板表达式应该返回目标属性所需类型的值。
别忘了方括号
一次性字符串初始化
当满足下列条件时,应该省略括号:
- 目标属性接受字符串值。
- 字符串是个固定值,可以直接合并到模块中。
- 这个初始值永不改变。
属性绑定还是插值表达式?
在多数情况下,插值表达式是更方便的备选项。实际上,在渲染视图之前,Angular 把这些插值表达式翻译成相应的属性绑定。
![]({{heroImageUrl}}) is the interpolated image.
会被自动翻译成
is the property bound image.
"{{title}}" is the interpolated title.
会被自动翻译成
"" is the property bound title.
当要渲染的数据类型是字符串时,基于可读性考虑,建议使用插值表达式。其他情况则必须使用属性绑定。
内容安全
不管是插值表达式还是属性绑定,都不会允许带有 script
标签的 HTML 泄漏到浏览器中。
插值表达式处理 script
标签与属性绑定有所不同,但是二者都只渲染没有危害的内容。
evilTitle = 'Template Syntax';
"{{evilTitle}}" is the interpolated evil title.
"" is the property bound evil title.
attribute、class 和 style 绑定
attribute 绑定
语法:[attr.attribute-name]
可以通过 attribute 绑定来直接设置 attribute 的值,因为当元素没有属性可绑的时候,就必须使用 attribute 绑定。
Three-Four
该语句会报错如下:
Template parse errors:
Can't bind to 'colspan' since it isn't a known native property
(模板解析错误:不能绑定到 'colspan',因为它不是已知的原生属性)
正确的写法:
One-Two
Five Six
attribute 绑定的主要用例之一是设置 ARIA attribute
ARIA 指可访问性,用于给残障人士访问互联网提供便利。
CSS 类绑定
语法:[class.class-name]
Bad curly special
Bad curly
The class binding is special
This one is not so special
通常更喜欢使用 ngClass
指令来同时管理多个类名。
样式绑定
语法:[style.style-property]
带有单位的绑定:
通常更喜欢使用 ngStyle
指令来同时设置多个内联样式。
事件绑定 (Event binding) (event)
语法:(目标事件)="模版语句"
(target event)="template statement"
目标事件
元素事件可能是更常见的目标,但 Angular 会先看这个名字是否能匹配上已知指令的事件属性,如:
click with myClick
如果这个名字没能匹配到元素事件或已知指令的输出属性,Angular 就会报“未知指令”错误。
$event 和事件处理语句(event handling statements)
在事件绑定中,Angular 会为目标事件设置事件处理器。当事件发生时,这个处理器会执行模板语句。典型的模板语句通常涉及到响应事件执行动作的接收器,例如从 HTML 控件中取得值,并存入模型。
In an event binding, Angular sets up an event handler for the target event. When the event is raised, the handler executes the template statement. The template statement typically involves a receiver, which performs an action in response to the event, such as storing a value from the HTML control into a model.
绑定会通过名叫 $event
的事件对象传递关于此事件的信息(包括数据值)。
事件对象的形态取决于目标事件。
The shape of the event object is determined by the target event.
如果目标事件是原生 DOM 元素事件, $event
就是 DOM事件对象,它有像 target
和 target.value
这样的属性。
如果事件属于指令,那么 $event
具体是什么由指令决定。
使用 EventEmitter 实现自定义事件
通常,指令使用 EventEmitter 来触发自定义事件。指令创建一个 EventEmitter
实例,并且把它作为属性暴露出来。指令调用 EventEmitter.emit(payload)
来触发事件,可以传入任何东西作为消息载荷。 父指令通过绑定到这个属性来监听事件,并通过 $event
对象来访问载荷。
示例
假设 HeroDetailComponent
用于显示英雄的信息,并响应用户的动作。 虽然 HeroDetailComponent
包含删除按钮,但它自己并不知道该如何删除这个英雄。 最好的做法是触发事件来报告“删除用户”的请求。
src/app/hero-detail.component.ts (template)
template: `
![]({{heroImageUrl}})
{{prefix}} {{hero?.name}}
`
src/app/hero-detail.component.ts (deleteRequest)
// This component make a request but it can't actually delete a hero.
deleteRequest = new EventEmitter();
delete() {
this.deleteRequest.emit(this.hero);
}
说明:组件定义了
deleteRequest
属性,它是EventEmitter
实例。 当用户点击删除时,组件会调用delete()
方法,让EventEmitter
发出一个Hero
对象。
现在,假设有个宿主的父组件,它绑定了 HeroDetailComponent
的 deleteRequest
事件。
当 deleteRequest
事件触发时,Angular 调用父组件的 deleteHero
方法, 在 $event
变量中传入要删除的英雄(来自 HeroDetail
)。
模板语句有副作用
模板语句的副作用不仅没问题,反而正是所期望的。
双向数据绑定 (Two-way binding) [(...)]
语法:[(x)]
[(x)]
语法结合了属性绑定的方括号 [x]
和事件绑定的圆括号 (x)
。当一个元素拥有可以设置的属性 x
和对应的事件 xChange
时,就可以使用 [(x)]
语法 。
示例 src/app/sizer.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'my-sizer',
template: `
`
})
export class SizerComponent {
@Input() size: number | string;
@Output() sizeChange = new EventEmitter();
dec() { this.resize(-1); }
inc() { this.resize(+1); }
resize(delta: number) {
this.size = Math.min(40, Math.max(8, +this.size + delta));
this.sizeChange.emit(this.size);
}
}
AppComponent.fontSize
被双向绑定到 SizerComponent
:
Resizable Text
Angular 将 SizerComponent
的绑定分解成这样:
说明:
$event
变量包含了SizerComponent.sizeChange
事件的荷载。 当用户点击按钮时,Angular 将$event
赋值给AppComponent.fontSizePx
。
内置指令
内置属性型指令(Built-in attribute directives)
属性型指令会监听和修改其它 HTML 元素或组件的行为、元素 Attribute、DOM Property。 它们通常会作为 HTML Attribute 的名称而应用在元素上。
常见的内置属性型指令:
-
NgClass
添加或移除一组CSS类 -
NgStyle
添加或移除一组CSS样式 -
NgModel
双向绑定到 HTML 表单元素
NgClass
示例:组件方法 setCurrentClasses
可以把组件的属性 currentClasses
设置为一个对象,它将会根据三个其它组件的状态为 true
或 false
而添加或移除三个类。
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 类:
This div is initially saveable, unchanged, and special
你既可以在初始化时调用
setCurrentClassess()
,也可以在所依赖的属性变化时调用。
NgStyle
ngStyle
需要绑定到一个 key:value
控制对象。 对象的每个 key
是样式名,它的 value
是能用于这个样式的任何值。
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'
};
}
This div is initially italic, normal weight, and extra large (24px).
NgModel
要使用 ngModel
需要导入 FormsModule
模块。
示例:
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 { }
更多关于
FormsModule
和ngModel
的内容参见表单。
使用 ngModel
实现双向数据绑定。
该语句实际上隐藏了其实现细节:
如果需要做一些不同的处理,就不能使用 [(ngModel)]
语法,而应写成扩展的方式:
ngModel
指令只能用在支持 ControlValueAccessor 的元素上。
内置结构型指令(Built-in structural directives)
结构型指令负责 HTML 布局。
常见的内置结构型指令:
-
NgIf
conditionally add or remove an element from the DOM -
NgFor
repeat a template for each item in a list -
NgSwitch
a set of directives that switch among alternative views
NgIf
You can add or remove an element from the DOM by applying an NgIf
directive to that element (called the host elment).
示例:
When the isActive
expression returns a truthy value, NgIf
adds the HeroDetailComponent
to the DOM. When the expression is falsy, NgIf
removes the HeroDetailComponent
from the DOM, destroying that component and all of its sub-components.
别忘了
ngIf
前面的星号(*
)。
这和显示/隐藏不是一回事。
我们也可以通过类绑定或样式绑定来显示或隐藏一个元素。但隐藏子树和用 NgIf
排除子树是截然不同的。
当隐藏子树时,它仍然留在 DOM 中。当 NgIf
为 false
时 Angular 从 DOM 中物理地移除了子树,它销毁了子树中的组件及其状态,也潜在释放了可观的资源。
防范空指针错误
NgIf
指令通常会用来防范空指针错误。 而显示/隐藏的方式是无法防范的。
Hello, {{currentHero.name}}
Hello, {{nullHero.name}}
currentHero
的名字只有当存在 currentHero
时才会显示出来。 而 nullHero
永远不会显示。
NgFor
NgFor
是一个重复器指令——显示列表项的一种方式。你先定义了一个 HTML 块,它规定了单个条目应该如何显示。然后你告诉 Angular 把这个块当做模板,渲染列表中的每个条目。
NgFor
is a repeater directive — a way to present a list of items. You define a block of HTML that defines how a single item should be displayed. You tell Angular to use that block as a template for rendering each item in the list.
例子1:
{{hero.name}}
例子2:
别忘了
ngFor
前面的星号(*
)。
赋值给 *ngFor
的字符串不是模板表达式,它是一个微语法 —— 由 Angular 自己解释的小型语言。
字符串 "let hero of heroes"
的含义:
取出 heroes
数组中的每个英雄,把它存入局部变量 hero
中,并在每次迭代时对模板 HTML 可用。
Take each hero in the heroes array, store it in the local hero looping variable, and make it available to the templated HTML for each iteration.
模板输入变量 (Template input variables)
The let
keyword before hero
creates a template input variable called hero
.
*ngFor 的索引 (index)
The index
property of the NgFor
directive context returns the zero-based index of the item in each iteration.
{{i + 1}} - {{hero.name}}
更多内容参见 NgFor API
*ngFor with trackBy
ngFor
指令有时候会性能较差,特别是在大型列表中。对一个条目的一丁点改动、移除或添加,都会导致级联的 DOM 操作。
有了 trackBy
,则只有 id
发生改变才会触发元素替换。
在组件中添加方法:
trackByHeroes(index: number, hero: Hero): number { return hero.id; }
使用 trackBy
:
({{hero.id}}) {{hero.name}}
NgSwitch
NgSwitch can display one element from among several possible elements, based on a switch condition.
NgSwitch 由三个指令组成:
- 属性型指令
NgSwitch
- 结构型指令
NgSwitchCase
- 结构型指令
NgSwitchDefault
示例:
NgSwitch
指令在增加或移除组件元素 (component elements) 时尤其有用。
模板引用变量 (Template reference variables) #var
模板引用变量通常是一个模版中的对 DOM 元素的一个引用。
A template reference variable is often a reference to a DOM element within a template.
使用井号 #
(或 ref-
)来声明一个模板引用变量。The#phone
declares a phone
variable on an element.
或者写成
你可以在模板中的任意位置引用该模板引用变量。
说明:
phone
refers to the phone numberbox. The phone button click handler passes the *input *value to the component's
callPhone
method.
模板引用变量如何获取自身的值?
通常,如果一个元素声明了一个模板引用变量,那么 Angular 会将模板引用变量的值设置为该元素的值。
In most cases, Angular sets the reference variable's value to the element on which it was declared.
示例:
{{submitMessage}}
If Angular hadn't taken it over when you imported the FormsModule
, it would be the HTMLFormElement. The heroForm
is actually a reference to an Angular NgForm directive with the ability to track the value and validity of every control in the form.
The native element doesn't have a form property. But the
NgForm
directive does, which explains how you can disable the submit button if the heroForm.form.valid
is invalid and pass the entire form control tree to the parent component's onSubmit
method.
注意
模板引用变量 (template reference variable) (#phone
) 与模板输入变量 (template input variable) (*ngFor
中的 let phone
) 并不相同。详见结构型指令。
输入输出属性 @Input
和 @Output
绑定目标与绑定源的区别:
- 绑定的目标是在
=
左侧的部分, 源则是在=
右侧的部分。 - 绑定的目标是绑定符:
[]
、()
或[()]
中的属性或事件名, 源则是引号" "
中的部分或插值符号{{}}
中的部分。 - 源指令中的每个成员都会自动在绑定中可用。 不需要特别做什么,就能在模板表达式或语句中访问指令的成员。
- 访问目标指令中的成员则受到限制。 只能绑定到那些显式标记为输入或输出的属性。
iconUrl
和 onSave
是绑定源
HeroDetailComponent.hero
是属性绑定的目标。 HeroDetailComponent.deleteRequest
是事件绑定的目标。
声明输入和输出属性
目标属性必须被显式的标记为输入或输出。
方法1:使用装饰器 @Input()
和 @Output()
。
@Input() hero: Hero;
@Output() deleteRequest = new EventEmitter();
方法2:通过元数据数组。
@Component({
inputs: ['hero'],
outputs: ['deleteRequest'],
})
两种方法不可同时使用。
输入还是输出?
输入属性通常接收数据值。 输出属性暴露事件生产者。
Input properties usually receive data values. Output properties expose event producers.
输入和输出这两个词是从目标指令的角度来说的。
- 从
HeroDetailComponent
角度来看,HeroDetailComponent.hero
是个输入属性, 因为数据流从模板绑定表达式流入那个属性。 - 从
HeroDetailComponent
角度来看,HeroDetailComponent.deleteRequest
是个输出属性, 因为事件从那个属性流出,流向模板绑定语句中的处理器。
给输入输出属性起别名
方法1:把别名传进 @Input
/ @Output
装饰器,就可以为属性指定别名:
@Output('myClick') clicks = new EventEmitter(); // @Output(alias) propertyName = ...
方法2:在 inputs
和 outputs
数组中为属性指定别名。 语法(属性名:别名)。
@Directive({
outputs: ['clicks:myClick'] // propertyName:alias
})
模板表达式操作符
管道操作符 |
管道是一个简单的函数,它接受一个输入值,并返回转换结果。
Title through uppercase pipe: {{title | uppercase}}
管道操作符会把它左侧的表达式结果传给它右侧的管道函数。
更多内容见管道。
还可以通过多个管道串联表达式:
Title through a pipe chain:
{{title | uppercase | lowercase}}
还能对管道使用参数:
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
json
管道对调试绑定特别有用:
输出结果:
{ "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 值。示例:
The current hero's name is {{currentHero?.name}}
说明:当
currentHero
为空时,保护视图渲染器,让它免于失败。
显示一个空 (null) 英雄的 name
示例:
The null hero's name is {{nullHero.name}}
:marked
JavaScript throws a null reference error, and so does Angular:
JavaScript 抛出了空引用错误,Angular 也是如此:code-example(format="nocode").
TypeError: Cannot read property 'name' of null in [null].
当 currentHero
为空的时候,应用崩溃了,整个视图都不见了。
笨重的解决办法1:
The null hero's name is {{nullHero.name}}
笨重的解决办法2:
The null hero's name is {{nullHero && nullHero.name}}
正确的解决办法:
The null hero's name is {{nullHero?.name}}
总结
透彻理解模板语法对开发至关重要。