模板

在 Angular 中,模板是用户界面 (UI) 片段的蓝图。模板是用 HTML 编写的,可以在模板中使用特殊语法来构建 Angular 的许多特性。

为了消除脚本注入攻击的风险,Angular 不支持模板中使用
script 元素。Angular 会忽略 script 标记,并向浏览器控制台输出一条警告。

插值语法

插值是指将表达式嵌入到被标记的文本中。默认情况下,插值使用双花括号 作为定界符。

在以下示例中,Angular 会求出 title 和 itemImageUrl 属性的值,以显示一些标题文本和图像。

1
2
<p>{{title}}</p>
<div><img alt="item" src="{{itemImageUrl}}"></div>

模板语句

模板语句是可在 HTML 中用于响应用户事件的方法或属性。

在以下示例中,模板语句 deleteHero() 出现在 = 号右侧的引号中,(event)=”statement”。

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

当用户单击 Delete hero 按钮时,Angular 就会调用组件类中 deleteHero() 方法。

可以将模板语句与元素、组件或指令一起使用以响应事件。

语句的上下文

语句上下文还可以引用模板自身的上下文属性。在下面的示例中,组件的事件处理方法 onSave() 将模板自己的 $event 对象用作参数。在接下来的两行中,deleteHero() 方法接收了模板输入变量 hero 作为参数,而 onSubmit() 接收了模板引用变量 #heroForm 作为参数。

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

在这个例子中,$event 对象、hero 和 #heroForm 的上下文都是其模板。

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

模板语句的上下文可以是组件类实例或模板。因此,模板语句无法引用全局名称空间中的任何内容,比如 window 或 document。比如,模板语句不能调用 console.log() 或 Math.max()。

绑定

在 Angular 模板中,绑定会在从模板创建的一部分 UI(DOM 元素、指令或组件)与模型(模板所属的组件实例)之间创建实时连接。此连接可用于将视图与模型同步、在视图中发生事件或用户操作时通知模型,或两者兼而有之。Angular 的变更检测算法负责保持视图和模型的同步。

模板表达式类似于 JavaScript 表达式。许多 JavaScript 表达式都是合法的模板表达式,但以下例外。

你不能使用那些具有或可能引发副作用的 JavaScript 表达式,包括:

  • 赋值 (=, +=, -=, …)

  • 运算符,比如 new、typeof 或 instanceof 等。

  • 链接表达式;或,

  • 自增和自减运算符:++ 和 –

  • 一些 ES2015+ 版本的运算符

和 JavaScript 语法的其它显著差异包括:

  • 不支持位运算,比如 | 和 &

  • 新的模板表达式运算符,比如 |

表达式上下文

插值表达式具有上下文 —— 表达式所属应用中的特定部分。通常,此上下文就是组件实例。

在下面的代码片段中,表达式 recommended 和 itemImageUrl2 表达式所引用的都是 AppComponent 中的属性。

1
2
<h4>{{recommended}}</h4>
<img alt="item 2" [src]="itemImageUrl2">

表达式还可以引用模板上下文中的属性,比如模板输入变量或模板引用变量。

下面的例子就使用了模板输入变量 customer。

1
2
3
<ul>
<li *ngFor="let customer of customers">{{customer.name}}</li>
</ul>

接下来的例子使用了模板引用变量 #customerInput。

1
2
3
<label>Type something:
<input #customerInput>{{customerInput.value}}
</label>

防止命名冲突

表达式估算的上下文是模板变量、指令的上下文对象(如果有)和组件成员的并集。如果你引用的名称属于这些命名空间之一,则 Angular 会应用以下优先逻辑来确定上下文:

模板变量的名称。

指令上下文中的名称。

组件成员的名称。

为避免变量遮盖另一个上下文中的变量,请保持变量名称唯一。在以下示例中,AppComponent 模板在问候 customer Padma。

然后,一个 ngFor 列出了 customers 数组中的每个 customer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
template: `
<div>
<!-- Hello, Padma -->
<h1>Hello, {{customer}}</h1>
<ul>
<!-- Ebony and Chiho in a list-->
<li *ngFor="let customer of customers">{{ customer.value }}</li>
</ul>
</div>
`
})
class AppComponent {
customers = [{value: 'Ebony'}, {value: 'Chiho'}];
customer = 'Padma';
}

ngFor 中的 customer 处于一个 的上下文中,所以它指向的是 customers 数组中的 customer,在这里是 Ebony 和 Chiho。此列表中不包含 Padma,因为那个 customer 位于 ngFor 以外的另一个上下文中。反之,

中的 customer 不包括 Ebony 或 Chiho,因为该 customer 的上下文是组件类,而这个类中 customer 的值是 Padma。

属性绑定(Property)

Angular 中的属性绑定可帮助你设置 HTML 元素或指令的属性值。使用属性绑定,可以执行诸如切换按钮、以编程方式设置路径,以及在组件之间共享值之类的功能。

绑定到属性

要绑定到元素的属性 ,请将其括在方括号 [] 内,这会将此属性标为目标属性。目标属性就是你要对其进行赋值的 DOM 属性 。

要为 image 元素的目标属性(src)赋值,请键入以下代码:

1
<img alt="item" [src]="itemImageUrl">

在大多数情况下,目标名称是 Property(属性)名称,即使它看起来是 Attribute(属性)名称。

在这个例子中,src 就是 元素的 Property 名称。

方括号 [] 使 Angular 将等号的右侧看作动态表达式进行求值。

如果不使用方括号,Angular 就会将右侧视为字符串字面量并将此属性设置为该静态值。

要将字符串赋值给属性,请键入以下代码:

1
<app-item-detail childItem="parentItem"></app-item-detail>

省略方括号就会渲染出字符串 parentItem,而不是 parentItem 的值。

Attribute 绑定

Attribute 绑定语法类似于 Property 绑定,但不是直接在方括号之间放置元素的 Property,而是在 Attribute 名称前面加上前缀 attr,后跟一个点 .。然后,使用解析为字符串的表达式设置 Attribute 值。

1
<p [attr.attribute-you-are-targeting]="expression"></p>

当表达式解析为 null 或 undefined 时,Angular 会完全删除该 Attribute。

绑定到 colspan

Attribute 绑定的另一个常见用例是绑定到表格中的 colspan Attribute。colspan Attribute 可帮助你以编程方式让表格保持动态。根据应用中用来填充表的数据量,某一行要跨越的列数可能会发生变化。

要将 Attribute 绑定到 的 colspan Attribute

使用以下语法指定 colspan:[attr.colspan]。

将 [attr.colspan] 设置为等于某个表达式。

在下面的示例中,我们将 colspan Attribute 绑定到表达式 1 + 1。

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

此绑定会导致 跨越两列。

样式(class和style)绑定

使用类和样式绑定从元素的 class 属性中添加和删除 CSS 类名,以及动态设置样式。

绑定到单个 CSS class

要创建单个类绑定,请键入以下内容:

[class.sale]=”onSale”

当绑定表达式 onSale 为真值时,Angular 会添加类,当表达式为假值时,它会删除类 —— undefined 除外。

绑定到多个 CSS 类

要绑定到多个类,请键入以下内容:

[class]=”classExpression”

表达式可以是以下之一:

  • 用空格分隔的类名字符串。

  • 以类名作为键名并将真或假表达式作为值的对象。

  • 类名的数组。

对于对象格式,Angular 会在其关联的值为真时才添加类。

如果同一类名有多个绑定,Angular 会根据样式优先级来确定要使用的绑定。

下表是各种类绑定语法的小结。

绑定类型 语法 输入属性 范例输入值
单一类绑定 [class.sale]=”onSale” boolean,undefined,null true, false
多重类绑定 [class]=”classExpression” string “my-class-1 my-class-2 my-class-3”
多重类绑定 [class]=”classExpression” Record<string, ‘boolean,undefined,null’> {foo: true, bar: false}
多重类绑定 [class]=”classExpression” Array<string> [‘foo’, ‘bar’]

绑定到单一样式

要创建单个样式绑定,请使用 style 前缀,后跟一个点和 CSS 样式的名称。

比如,要设置 ‘width’ 样式,请键入以下内容:[style.width]=”width”

Angular 将该属性设置为绑定表达式的值,这通常是一个字符串。(可选)你可以添加单位扩展名,比如 em 或 %,这需要数字类型。

绑定到多个样式

要切换多个样式,请绑定到 [style] Attribute,比如 [style]=”styleExpression”。styleExpression 可以是如下格式之一:

样式的字符串列表,比如 “width: 100px; height: 100px; background-color: cornflowerblue;”。

一个对象,其键名是样式名,其值是样式值,比如 {width: ‘100px’, height: ‘100px’, backgroundColor: ‘cornflowerblue’}。

注意,不支持把数组绑定给 [style]

当把 [style] 绑定到对象表达式时,该对象的引用必须改变,这样 Angular 才能更新这个类列表。在不改变对象引用的情况下更新其属性值是不会生效的。

单样式和多样式绑定示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component({
selector: 'app-nav-bar',
template: `
<nav [style]='navStyle'>
<a [style.text-decoration]="activeLinkStyle">Home Page</a>
<a [style.text-decoration]="linkStyle">Login</a>
</nav>`
})
export class NavBarComponent {
navStyle = 'font-size: 1.2rem; color: cornflowerblue;';
linkStyle = 'underline';
activeLinkStyle = 'overline';
/* . . . */
}

下表是各种样式绑定语法的小结。

绑定类型 语法 输入属性 范例输入值
单一样式绑定 [style.width]=”width” string, undefined,null “100px”
带单位的单一样式绑定 [style.width.px]=”width” number,undefined,null 100
多重样式绑定 [style]=”styleExpression” string “width: 100px; height: 100px”
多重样式绑定 [style]=”styleExpression” Record<string, string,undefined,null> {width: ‘100px’, height: ‘100px’}

事件绑定

通过事件绑定,你可以侦听并响应用户操作,比如按键、鼠标移动、点击和触摸。

绑定到事件

要绑定到事件,你可以使用 Angular 事件绑定语法。此语法由等号左侧括号中的目标事件名称和右侧带引号的模板语句组成。

创建以下示例;目标事件名是 click,模板语句是 onSave()。

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

事件绑定侦听按钮的单击事件,并在发生单击时调用组件的 onSave()。

绑定到被动事件

这是一项高级技术,对于大多数应用程序来说不是必需的。如果你想优化导致性能问题的频繁发生的事件,可能会发现这很有用。

Angular 还支持被动事件侦听器。比如,使用以下步骤使滚动事件变为被动的。

在 src 目录下创建一个文件 zone-flags.ts。

将以下行添加到此文件中。

1
(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll'];

在 src/polyfills.ts 文件中,在导入 zone.js 之前,先导入新创建的 zone-flags。

1
2
import './zone-flags';
import 'zone.js'; // Included with Angular CLI.

在这些步骤之后,如果你为 scroll 事件添加事件侦听器,侦听器就会是 passive 的。

绑定到键盘事件

你可以用 Angular 的绑定语法绑定到键盘事件。你可以指定要绑定到键盘事件的键值或代码。它们的 key 和 code 字段是浏览器键盘事件对象的原生部分。默认情况下,事件绑定假定你要使用键盘事件上的 key 字段。你还可以用 code 字段。

键的组合可以用点(.)分隔。例如, keydown.enter 将允许你将事件绑定到 enter 键。你还可以用修饰键,例如 shift 、 alt 、 control 和 Mac 中的 command 键。以下示例展示了如何将键盘事件绑定到 keydown.shift.t 。

1
<input (keydown.shift.t)="onKeydown($event)" />

根据操作系统的不同,某些组合键可能会创建特殊字符,而不是你期望的组合键。例如,当你同时使用 option 和 shift 键时,MacOS 会创建特殊字符。如果你绑定到 keydown.shift.alt.t ,在 macOS 上,该组合会生成 ˇ 而不是 t ,它与绑定不匹配,也不会触发你的事件处理程序。要绑定到 macOS 上的 keydown.shift.alt.t ,请使用 code 键盘事件字段来获取正确的行为,例如此示例中显示的 keydown.code.shiftleft.altleft.keyt 。

1
<input (keydown.code.shiftleft.altleft.keyt)="onKeydown($event)" />

code 字段比 key 字段更具体。 key 字段总是会报告 shift ,而 code 字段将指明 leftshift 或 rightshift 。使用 code 字段时,你可能需要添加单独的绑定以捕获你想要的所有行为。使用 code 字段时无需处理操作系统特有的行为,例如 macOS 上的 shift + option 行为。

有关更多信息,请访问键值和键码的完整参考,以帮助你构建事件字符串。

双向绑定(组件)

双向绑定为应用中的组件提供了一种共享数据的方式。使用双向绑定绑定来侦听事件并在父组件和子组件之间同步更新值。

添加双向数据绑定

Angular 的双向绑定语法是方括号和圆括号的组合 [()]。[] 进行属性绑定,() 进行事件绑定,如下所示。

1
<app-sizer [(size)]="fontSizePx"></app-sizer>

双向绑定工作原理

为了使双向数据绑定有效,@Output() 属性的名字必须遵循 inputChange 模式,其中 input 是相应 @Input() 属性的名字。比如,如果 @Input() 属性为 size,则 @Output() 属性必须为 sizeChange。

后面的 sizerComponent 具有值属性 size 和事件属性 sizeChange。size 属性是 @Input(),因此数据可以流入 sizerComponent。sizeChange 事件是一个 @Output(),它允许数据从 sizerComponent 流出到父组件。

接下来,有两个方法,dec() 用于减小字体大小,inc() 用于增大字体大小。这两种方法使用 resize() 在最小/最大值的约束内更改 size 属性的值,并发出带有新 size 值的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
export class SizerComponent {

@Input() size!: number | string;
@Output() sizeChange = new EventEmitter<number>();

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);
}
}

sizerComponent 模板有两个按钮,分别将 click 事件绑定到 inc() 和 dec() 方法。当用户单击按钮之一时,sizerComponent 调用相应的方法。inc() 和 dec() 这两个方法分别使用 +1 或 -1 调用 resize() 方法,它使用新的 size 值引发 sizeChange 事件。

1
2
3
4
5
<div>
<button type="button" (click)="dec()" title="smaller">-</button>
<button type="button" (click)="inc()" title="bigger">+</button>
<span [style.font-size.px]="size">FontSize: {{size}}px</span>
</div>

在 AppComponent 模板中,fontSizePx 被双向绑定到 SizerComponent。

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

在 AppComponent 中,通过将 fontSizePx 的值设置为 16 来设置初始 SizerComponent.size 值。

1
fontSizePx = 16;

单击这些按钮将更新 AppComponent.fontSizePx。修改后的 AppComponent.fontSizePx 值将更新样式绑定,从而使显示的文本变大或变小。

双向绑定语法是属性绑定和事件绑定的组合的简写形式。拆成单独的属性绑定和事件绑定形式的 SizerComponent 代码如下。

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

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

表单中的双向绑定

因为没有任何原生 HTML 元素遵循了 x 值和 xChange 事件的命名模式,所以与表单元素进行双向绑定需要使用 NgModel。

模板变量

模板变量可以帮助你在模板的另一部分使用这个部分的数据。使用模板变量,你可以执行某些任务,比如响应用户输入或微调应用的表单。

模板变量可以引用这些东西:

  • 模板中的 DOM 元素

  • 指令或组件

  • 来自 ng-template 的 TemplateRef

  • Web 组件

在模板中,要使用井号 # 来声明一个模板变量。下列模板变量 #phone 声明了一个名为 phone 的变量,其值为此 <input> 元素。

1
<input #phone placeholder="phone number" />

可以在组件模板中的任何地方引用某个模板变量。这里的 <button> 就引用了 phone 变量。

1
2
3
4
5
6
<input #phone placeholder="phone number" />

<!-- lots of other elements -->

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

Angular 是如何为模板变量赋值的

Angular 根据你所声明的变量的位置给模板变量赋值:

  • 如果在组件上声明变量,该变量就会引用该组件实例。

  • 如果在标准的 HTML 标记上声明变量,该变量就会引用该元素。

  • 如果你在 元素上声明变量,该变量就会引用一个 TemplateRef 实例来代表此模板。

指定名称的变量

如果该变量在右侧指定了一个名字,比如 #var=”ngModel”,那么该变量就会引用所在元素上具有这个 exportAs 名字的指令或组件。

将 NgForm 与模板变量一起使用

在大多数情况下,Angular 会把模板变量的值设置为它所在的元素。在前面的例子中,phone 引用的是电话号码 <input>。该按钮的 click 处理程序会把这个 <input> 的值传给该组件的 callPhone() 方法。

这里的 NgForm 指令演示了如何通过引用指令的的 exportAs 名字来引用不同的值。在下面的例子中,模板变量 itemForm 在 HTML 中分别出现了三次。

1
2
3
4
5
6
7
8
9
<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">
<label for="name">Name</label>
<input type="text" id="name" class="form-control" name="name" ngModel required />
<button type="submit">Submit</button>
</form>

<div [hidden]="!itemForm.form.valid">
<p>{{ submitMessage }}</p>
</div>

如果没有 ngForm 这个属性值,itemForm 引用的值将是 HTMLFormElement 也就是 <form> 元素。如果某元素是一个 Angular 组件,则不带属性值的引用会自动引用此组件的实例。否则,不带属性值的引用会引用此 DOM 元素,而不管此元素上有一个或多个指令。

模板变量的作用域

就像 JavaScript 或 TypeScript 代码中的变量一样,模板变量的范围为声明它们的模板。

同样,诸如 *ngIf 和 *ngFor 类的结构指令或 <ng-template> 声明会创建一个新的嵌套模板范围,就像 JavaScript 的控制流语句(例如 if 和 for 创建新的词法范围。你不能从边界外访问这些结构指令之一中的模板变量。

在嵌套模板中访问

内部模板可以访问外模板定义的模板变量。

在下面的例子中,修改 <input> 中的文本值也会改变 <span> 中的值,因为 Angular 会立即通过模板变量 ref1 来更新这种变化。

1
2
<input #ref1 type="text" [(ngModel)]="firstExample" />
<span *ngIf="true">Value: {{ ref1.value }}</span>

在这种情况下,<span> 上的 *ngIf 会创建一个新的模板范围,其中包括其父范围中的 ref1 变量。

但是,从外部的父模板访问子范围中的变量是行不通的。

1
2
<input *ngIf="true" #ref2 type="text" [(ngModel)]="secondExample" />
<span>Value: {{ ref2?.value }}</span> <!-- doesn't work -->

在这里,ref2 是在 *ngIf 创建的子范围中声明的,并且无法从父模板访问。

模板输入变量

模板输入变量是一个具有在创建该模板实例时设置的值的变量。

可以在 NgFor 的长格式用法中看到模板输入变量的作用:

1
2
3
4
5
<ul>
<ng-template ngFor let-hero [ngForOf]="heroes">
<li>{{hero.name}}
</ng-template>
</ul>

NgFor 指令将实例化此为 hero 数组中的每个 heroes 一次,并将为每个实例相应地设置 hero 变量。

实例化 <ng-template> 时,可以传递多个命名值,这些值可以绑定到不同的模板输入变量。输入变量的 let- 声明的右侧可以指定应该用于该变量的值。

例如,NgFor 还提供了对数组中每个英雄的 index 的访问:

1
2
3
4
5
<ul>
<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
<li>Hero number {{i}}: {{hero.name}}
</ng-template>
</ul>