angular学习笔记4-模板语法

终睿
2023-12-01

1,

模型-视图-控制器 (MVC) 或模型-视图-视图模型 (MVVM) 

在 Angular 中,组件扮演着控制器或视图模型的角色,模板则扮演视图的角色

模板中的 HTML

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

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

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

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

所谓 "插值" 是指将表达式嵌入到标记文本中。 默认情况下,插值表达式会用双花括号 {{和 }} 作为分隔符。

在下面的代码片段中,{{ currentCustomer }} 就是插值表达式的例子。

<h3>Current customer: {{ currentCustomer }}</h3>在括号之间的“素材”,通常是组件属性的名字。Angular 会用组件中相应属性的字符串值一般来说,括号间的素材是一个模板表达式,Angular 先对它求值,再把它转换成字符串

插值其实是一个特殊语法,Angular 会把它转换为属性绑定。

如果你想用别的分隔符来代替 {{ 和 }},也可以通过 Component 元数据中的 interpolation 选项来配置插值分隔

 

模板表达式

模板表达式会产生一个值,并出现在双花括号 {{ }} 中。 Angular 执行这个表达式,并把它赋值给绑定目标的属性,这个绑定目标可能是 HTML 元素、组件或指令。

表达式上下文

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

<h4>{{recommended}}</h4>
<img [src]="itemImageUrl2">

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

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

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

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

在 {{customer.name}} 表达式中的 customer 实际引用的是模板变量,而不是组件的属性。

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

模板语句

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

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

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

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

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

语句上下文

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

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

语句上下文可以引用模板自身上下文中的属性。 在下面的例子中,就把模板的 $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 属性。

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

绑定语法:概览

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

单向
从数据源
到视图

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

插值
属性
Attribute
CSS 类
样式

从视图到数据源的单向绑定

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

事件

双向

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

 

 

 

双向 

HTML attribute 与 DOM property 的对比

要想理解 Angular 绑定如何工作,重点是搞清 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 看起来映射到了 property…… 但却不像你想的那样!

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

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 的 value 这个 attribute 指定了初始值;DOM 的 value 这个 property 是当前值。

disabled 这个 attribute 是另一种特例。按钮的 disabled 这个 property 是 false,因为默认情况下按钮是可用的。 当你添加 disabled 这个 attribute 时,只要它出现了按钮的 disabled 这个 property 就初始化为 true,于是按钮就被禁用了。

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

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

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

这句话值得再强调一次: 模板绑定是通过 property 和事件来工作的,而不是 attribute

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

绑定目标

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

绑定类型

目标

范例

属性

元素的 property
组件的 property
指令的 property

src/app/app.component.html

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

事件

元素的事件
组件的事件
指令的事件

src/app/app.component.html

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

双向

事件与 property

src/app/app.component.html

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

Attribute

attribute(例外情况)

src/app/app.component.html

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

CSS 类

classproperty

src/app/app.component.html

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

样式

styleproperty

src/app/app.component.html

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

属性绑定 ( [属性名] )

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

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

<img [src]="heroImageUrl">

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

content_copy<img bind-src="heroImageUrl">

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

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

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

单向输入

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

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

别忘了方括号

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

不要出现这样的失误:

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

属性绑定还是插值?

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

<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>

在多数情况下,插值是更方便的备选项。

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

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

attribute 绑定

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

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

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

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

考虑 ARIA, SVG 和 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

正如提示中所说,<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>

CSS 类绑定

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>

样式绑定

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

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

content_copy<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>

事件绑定 (event)

事件绑定允许你侦听某些事件,比如按键、鼠标移动、点击和触屏

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

目标事件

如前所述,其目标就是此按钮的单击事件。

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

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

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

$event 和事件处理语句

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

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

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

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

<input [value]="currentItem.name"
       (input)="currentItem.name=$event.target.value" >
without NgModel

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

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

使用 EventEmitter 实现自定义事件

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

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

下面的代码节选自 HeroDetailComponent

src/app/item-detail/item-detail.component.html (template)

<img src="{{itemImageUrl}}" [style.display]="displayNone">
<span [style.text-decoration]="lineThrough">{{ item.name }}
</span>
<button (click)="delete()">Delete</button>

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

// This component makes a request but it can't actually delete a hero.
@Output() deleteRequest = new EventEmitter<Item>();

delete() {
  this.deleteRequest.emit(this.item);
  this.displayNone = this.displayNone ? '' : 'none';
  this.lineThrough = this.lineThrough ? '' : 'line-through';
}

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

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

 

content_copy<app-item-detail (deleteRequest)="deleteItem($event)" [item]="currentItem"></app-item-detail>

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

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

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

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

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-sizer',
  template: `
  <div>
    <button (click)="dec()" title="smaller">-</button>
    <button (click)="inc()" title="bigger">+</button>
    <label [style.font-size.px]="size">FontSize: {{size}}px</label>
  </div>`
})
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);
  }
}

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

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

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

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

如果能在像 <input> 和 <select> 这样的 HTML 元素上使用双向数据绑定就更好了。 可惜,原生 HTML 元素不遵循 x 值和 xChange 事件的模式。

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

内置属性型指令

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

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

  • NgClass - 添加或移除一组 CSS 类

  • NgStyle - 添加或移除一组 CSS 样式

  • NgModel - 双向绑定到 HTML 表单元素

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 设置为一个对象,它将会根据三个其它组件的状态为 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 类:

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

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

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)]双向绑定到表单元素

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

 

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

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

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

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

内置结构型指令

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

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

  • NgIf - 根据条件把一个元素添加到 DOM 中或从 DOM 移除

  • NgSwitch 一组指令,用来在多个可选视图之间切换。

  • NgForOf - 对列表中的每个条目重复套用同一个模板

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

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

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

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

带索引的 *ngFor

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

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

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

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

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

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

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

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

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

  • 如果没有 trackBy,这些按钮都会触发完全的 DOM 元素替换。

  • 有了 trackBy,则只有修改了 id 的按钮才会触发元素替换。

NgSwitch 指令

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

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

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

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

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

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

  • NgSwitchCase 会在它绑定到的值等于候选值时,把它所在的元素加入到 DOM 中。

  • NgSwitchDefault 会在没有任何一个 NgSwitchCase 被选中时把它所在的元素加入 DOM 中。

模板引用变量 ( #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>

声明输入与输出属性

在本章的例子中,绑定到 HeroDetailComponent 不会失败,这是因为这些要进行数据绑定的属性都带有 @Input() 和 @Output() 装饰器。

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

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

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

输入还是输出?

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

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

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

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

模板表达式操作符

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

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

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

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

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

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

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

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

如果类型检查器在运行期间无法确定一个变量是 null 或 undefined,那么它也会抛出一个错误。 你自己可能知道它不会为空,但类型检查器不知道。 所以你要告诉类型检查器,它不会为空,这时就要用到非空断言操作符

Angular 模板中的**非空断言操作符(!)也是同样的用途。

例如,在用*ngIf来检查过 hero 是已定义的之后,就可以断言 hero 属性一定是已定义的。

content_copy<!--No hero, no text -->
<div *ngIf="hero">
  The hero's name is {{hero!.name}}
</div>

安全导航操作符不同的是,非空断言操作符不会防止出现 null 或 undefined。 它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 "严格空值检测"。

内置模板函数

类型转换函数 $any()

有时候,绑定表达式可能会在 AOT 编译时报类型错误,并且它不能或很难指定类型。要消除这种报错,你可以使用 $any()转换函数来把表达式转换成 any 类型,范例如下:

content_copy<p>The item's undeclared best by date is: {{$any(item).bestByDate}}</p>

当 Angular 编译器把模板转换成 TypeScript 代码时,$any 表达式可以防止 TypeScript 编译器在进行类型检查时报错说 bestByDate 不是 item 对象的成员。

$any() 转换函数可以和 this 联合使用,以便访问组件中未声明过的成员。

src/app/app.component.html

content_copy<p>The item's undeclared best by date is: {{$any(this).bestByDate}}</p>

$any() 转换函数可以用在绑定表达式中任何可以进行方法调用的地方。

 

 

 

 

 

 

 

 

 类似资料: