大家都熟悉MVC或MVVM,在Angular,组件类相当于控制器和视图模型,而模板表示视图。面向用户只有视图,所以如何使用Angular提供的模板引擎非常重要。

一、HTML

HTML是Angular模板语言,除 <script><html><body><base> 外都是合法的模板语法。

我们也可能通过组件和指令来扩展HTML语言能力。

二、插值绑定

插值绑定利用两对大括号来表示:

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

也可以放在HTML元素某属性里面:

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

大括号里通常是组件类属性名,Angular会替换属性具体值。

更普遍的是,大括号里一个模板表达式,Angular会自行转化成一个字符串,下面是一个1+1的插值表达式写法:

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

表达式还可以和组件类方法结合。

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

简单的说插件绑定会根据的表达式放在两对大括号里,根据计算结果后把值转换成字符串,最后,将结果分配给元素。在讨论更多细节前,还得先了解模板表达式与语法。

三、模板表达式

模板表达式最终结果是一个值,并把结果分配到绑定目标的属性上,目标可以是HTML元素、组件、指令。

{{1 + 1}} 就是一个表达式。在【属性绑定】小节也会看到这类表达式,它出现在属性 = 号的右边。

模板表达式看起来像在写JavaScript,但不是所有JavaScript表达式都是合法的,有一些引起副作用被禁止使用,包括:

  • 赋值表达式(=+=-=
  • new 操作符
  • 链式表达式 ;,
  • 递增和递减(++--

和JavaScript语法有差异的:

  • 不支持位运算符(|&
  • 新模板表达式操作符(|?.

上下文

也许更让人吃惊,模板表达式无法访问诸如 windowdocumentconsole.logMath.max 等之类的任何全局命令空间对象,通常仅限于组件实例对象。

{{title}} 表达式中 title 就是来自组件类属性,同样 [disabled]="isUnchanged" 属性绑定中的 isUnchanged 也是来自组件类。

模板表达式还可以访问本地模板变量,后续会讲到。

注意事项

没有明显副作用

模板表达式不应该更改目标属性值以外的任何应用程序状态。

Angular有一条非常重要的规则策略:单向数据流,视图应该是稳定的,模板表达式都不应该在读取数据过程中改变其他显示值。其实要想做到这一点也很困难,因为表达式的上下文非常严谨。

快速执行结果

对一些像鼠标移动、拖拽等高频率操作都有可能会倒置表达式重新计算,所以表达式非常快的完成,否则很难想象用户体验。

简单

表达式可以写得非常复杂,但不建议这么做;最好只有属性或方法。否则,很难做测试。

幂等性

最理想是幂等表达式,因为它没副作用,而且可以提高Angular变化检测性能。

在Angular中,幂等表达式值只会在它所依赖的值变化时才会变化。

在Event Loop(是一种单线程的运行机制)循环过程中,依赖值不应该改变。幂等表达式如果返回一个字符串、数字、对象(包括日期或数据),则连续两次调用时返回的都应该是相同的字符串、数字、对象引用。

四、模板语句

一个模板语句会根据HTML元素、组件、指令所绑定的目标做出相应事件响应。

可以看看 (event)="statement" 这么一个事件绑定,其 = 右边就是模板语句。

模板语句有个副作用,即如何对用户输入做处理?其实没有必须对事件做响应。

在Angular里,事件响应是另外一种单身数据流,依然是在Event Loop循环过程检测状态值变化,可在任何回合中自由改变任何事情。

类似模板表达式,模板语法也是看起来像JavaScript表达式。模板语法解析器不同于模板表达式解析器,他是支持赋值运算符 = 和链式表达式 ;,

不过,也包括一些禁用JavaScript语法:

  • new 操作符
  • 递增和递减(++--
  • 赋值表达式(=+=-=
  • 位运算 |&
  • 模板表达式操作符(|?.

上下文

与模板表达式一样,无法访问诸如 windowdocumentconsole.logMath.max 等之类的任何全局命令空间对象,通常仅限于组件实例对象。

(click)="onSave()"onSave 就是来自组件类方法。

模板语句也可以访问本地模板变量,后续会讲到。

注意事项

与表达式一样尽量避免复杂模板语句。

五、绑定语法

数据绑定是一种将应用数据转换用户可视的协调机制。虽然可以从HTML推拉值,但如果能够用简单的声明绑定,而让框架做这些傻不拉叽的工作,那不是很美好嘛。

Angular提供多种数据绑定方式,后续会逐一讨论,现在从一个高层次角度先来看看数据绑定语法。

根据数据流向分为三大类:

数据方向 语法 绑定类型
数据源到视图 {{expression}}
[target] = “expression”
bind-target = “expression”
插值法
Property DOM属性
Attribute HTML特性
CSS类名属性
Style 样式
视图到数据源 (target) = “statement”
on-target = “statement”
事件
双向绑定 [(target)] = “expression”
bindon-target = “expression”
双向

与插值绑定相比,其它绑定类型在 = 左边目标名称,被 []() 符合包裹或特殊前缀(bind-on-bindon-)。

中间有几百个单词用来说服我们接受这种变异,反正说服不说服都要认可

DOM属性(Property)与HTML特性(Attribute)的区别

二者区别至关重要,这助于理解Angular绑定过程。

特性(Attribute)是定义在HTML上面,而属性(Property)定义在DOM上面。

  • 少数HTML特性1:1和DOM属性映射,id 就是一个例子。
  • 一些HTML特性没有和DOM属性映射,colspan 就是一个例子。
  • 一些DOM属性没有和HTML特性映射,textContent就是一个例子。
  • 大部分HTML特性看起来和DOM属性有映射,但又似乎。

最后一项让人很容易混淆,但是如果你理解下面这条重要规则就清晰很多:HTML特性初始化时会把DOM属性一并初始化,但DOM属性的修改是不会倒置HTML特性的修改

举个栗子,当浏览器渲染 <input type="text" value="Bob"> 时,会同时创建一个DOM属性 value 其值为 “Bod”。然而当用户在文本框里重新输入 “Sally”,DOM属性 value 变成 “Sally”,但HTML元素中 value 属性依然还是 “Bod”。

HTML特性的 value 是初始化值,而DOM value 才是最新值。

disabled 又是另一种特例,因为 disabled 他并不受其值管控,比如不管你的 disabled="true"disabled="false" 其按钮都是不可用状态。

DOM属性与HTML特性不是一个东西,即使英文看起来一样。

总之一条很重要的规则:

模板绑定的目标是DOM属性与事件,非HTML特性
模板绑定的目标是DOM属性与事件,非HTML特性
模板绑定的目标是DOM属性与事件,非HTML特性

恩,重要的事说三遍,这对我们加深理解模板绑定极为重要。

然而,在Angular2世界里,HTML特性只是用来初始化元素和指令状态。当我们绑定数据时,只处理元素、指令属性、事件。

绑定目标

一个数据绑定的目标是一些DOM属性。这些类型可能是元素、组件、指令属性或元素、组件、指令事件或DOM属性名。

DOM属性绑定

<!--元素属性-->
<img [src]="heroImageUrl">
<!--组件属性-->
<hero-detail [hero]="currentHero"></hero-detail>
<!--指令属性-->
<div [ngClass]="{selected: isSelected}"></div>

事件绑定

<!--元素事件-->
<button (click)="onSave()">Save</button>
<!--组件事件-->
<hero-detail (deleteRequest)="deleteHero()"></hero-detail>
<!--指令事件-->
<div (myClick)="clicked=$event">click me</div>

双向绑定

<!--事件和属性-->
<input [(ngModel)]="heroName">

HTML特性绑定

前面说Angular2的世界里都是指DOM属性绑定,为什么还会提供HTML特性绑定,这是因为HTML特性没有相应的DOM属性,比如:aria

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

Css类名绑定

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

Style样式绑定

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

接下来详细介绍每一个绑定类型。

注:如无特殊注明,后面说到的属性绑定都泛指DOM属性。

六、属性绑定

当我们想根据模板表达式的值来设置视图元素属性时,就可以用属性绑定了。

最常见属性绑定是将元素属性值设置为组件属性值,比如图像元素的 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>

有去无回

有时把属性绑定描述成单向数据绑定,因为它是单一方向数据流,从一个组件的数据属性至目标元素属性。

我们无法使用属性绑定,从目标元素获取值,只能设置值。也无法使用属性绑定来调用目标元素的方法(比如:button的click())。

绑定目标

中括号里指定绑定目标属性名,例如图像元素的 src 属性:

<img [src]="heroImageUrl">

或者使用 bind- 前缀:

<img bind-src="heroImageUrl">

目标属性名始终都是DOM属性,虽然有些名称(比如:src)看起来还是在操作HTML特征,那只不过 src 的DOM属性和HTML特性是一样的名字。

元素属性是非常常见目标属性,但是Angular会优先查找已知指令。比如:

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

如果已知指令和元素都无法匹配就会报告 “unknown directive” 异常。

避免副作用

正如之前讨论过那样,一个模板表达式应该是没有副使用,虽然Angular语言本身已经做了诸多限制,比如不能实例化或递增或递减。当然,如果非要做一些产生副作用的事,Angular也是无法知道的。比如通过表达式调用一下 getFoo() 方法时,如果方法内有对其他状态值做修改时,Angular在检测过程会提示一个警告信息。

所以建议:坚持使用数据属性或方法只做数据返回

返回适合类型值

就是说目标属性要求是一个String你就返回一个String,Number你就返回Number,Object返回Object……其实这一点还是要非常注意,因为对于HTML属性的包容性相对于强一点,如果是组件属性绑定那就傻B了。

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

记得括号

Angular解析模板时,只认中括号,所以别跟Angular对着干……

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

字符串初始化

当满足下所有情况时,应该省略中括号:

  • 目标属性接受字符串值
  • 在模板里是一个固定值
  • 初始化的值永远不会变

我们经常初始化一些标准HTML属性,如同初始化组件和组件属性一样。比如 HeroDetailComponentprefix 属性是一个固定字符串,而非模板表达式;这些Angular会给组件设置值,然后再也不会跟他再打交道。

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

属性绑定还是插值绑定?

我们经常要在属性绑定还是插值绑定做选择,比如下面的绑定部分结果都一样:

Interpolated: <img src="{{heroImageUrl}}"><br>
Property bound: <img [src]="heroImageUrl">

  <div>The interpolated title is {{title}}</div>
  <div [textContent]="'The [textContent] title is '+title"></div>

插值绑定更优雅,事实上,Angular在做转换时会把插值绑定统一转换成属性绑定。技术上并没有倾向哪一种,关键还是代码可读性。

七、样式绑定

模板语法还为那些不太适合属性绑定场景提供一些特殊的单向绑定。

HTML特性绑定

此小节属性绑定泛指HTML特性属性

前面说过属性绑定目标都是DOM属性,为什么这里还会有这个东西?(如果按我的翻译中文字面意思来看二者的属性绑定和HTML特性绑定是两个不同东西,但如果是英文二者的意思都是属性,难于区分)

因为诸如像 ARIASVG 他们并没有相应的DOM属性。而此时我们的视图需要根据组件类的值动态渲染时,就需要用到了。

按之前所学,很容易就可以想到用这么来写绑定:

<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 DOM属性,而属性绑定又只能设置DOM属性,所以就异常了呀。

所以Angular提供了一个特别的语法,就是用 attr. 做为目标属性的前缀,比如:

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

另一个设置ARIA属性示例:

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

CSS类名绑定

添加或移除CSS类名就用CSS类名绑定,它很像属性绑定,也是用中括号,以 class 前缀开始,再加 .,紧跟CSS类名,如:[class.class-name]= 号的右边是一个表达式,结果是一个布尔类型,如果结果值为真表达CSS类名可用,反之,不可用。

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

如果不指定CSS类名,那么意味者替换整个 class

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

但它只能处理一个CSS类名,后面还会介绍 NgClass 指令来管理多个CSS类名。

Style样式绑定

设置内联样式就用Style样式绑定。和CSS类名绑定一样,只不过前缀变成 style,CSS类名变成CSS样式属性,如:[style.style-property]

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

有一些样式属性带有单位的,则需要把单位加到样式属性名后面。

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

注:如果遇到一些像 font-size 的样式属性,则统一用驼峰式命令规则。

如同CSS类名绑定一样,也有一个 NgStyle 指令来管理多个内联样式属性。

八、事件绑定

当目前为止,我们遇到数据流方向都是:从组件到元素

然而用户不光只看屏幕内容,他还得交互呀,比如:输入呀、点击按钮呀,这些的数据流是 从元素到组件

了解用户操作行为只能透过事件,比如:按键盘、鼠标移动、点击、触摸,程序透过Angular事件绑定声明我们感觉兴趣的用户行为。

事件绑定语法:用一对小括号包裹着目标事件名,等号的右边引号内容是一模板语法。以下是一个监听按钮点击事件,当点击触发时调用组件 onSave() 方法:

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

目标事件

用一对小括号包裹事件名,比:(click)

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

也可以用 on- 前缀:

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

元素的事件是最常见绑定目标,但是Angular在匹配事件目标时会优先查找已知的指令名,比如:

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

如果在已知指令和元素事件都无法匹配时,就抛出一个 “unknown directive” 错误。

$event和事件处理语句

在事件绑定中,Angular为目标事件设置事件处理程序,当事件触发时会被执行。模板语句在事件响应时通常涉及做一些事情,如把HTML控件值存储到模型当中。

事件绑定通过 $event 对象承载事件相关信息,包括数据值。其类型也是由目标事件本身决定,如果目标事件是原生DOM元素事件则 $event 值为 DOM事件对象,含诸如 targettarget.value 属性。

考虑以下示例:

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

文本框 value 绑定了一个 firstName 属性,同时绑定 input 事件用于监听内容变化。当用户改变文本框内容时,会执行模板语句。这里是根据 $event.target.value 获取的值,更新至 firstName 属性值。

如果绑定事件是一个指令(记得:组件也是指令),那 $event 的值就是相应的指令类型。

使用EventEmitter自定义事件

指令通常使用EventEmitter来触发自定义事件,每个指令会创建一些暴露类型为 EventEmitter 的属性,然后调用 EventEmitter.emit(payload) 触发事件,传入的 payload 消息可以是任意类型。父指令通过事件绑定监听,并把 $event 对象通过 payload 参数传递给父组件。

例如 HeroDetailComponent 展示 hero 信息并响应用户的操作,虽然 HeroDetailComponent 有一个删除按钮,但不知道如何删除它。最好做好就是通过事件,说:“爷爷我要把自己删除了”。

以下是 HeroDetailComponent 关键代码:

template: `
<div>
  <img src="{{heroImageUrl}}">
  <span [style.text-decoration]="lineThrough">
    {{prefix}} {{hero?.fullName}}
  </span>
  <button (click)="delete()">Delete</button>
</div>`
// This component make a request but it can't actually delete a hero.
deleteRequest = new EventEmitter<Hero>();

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

组件定义一个类型为 EventEmitterdeleteRequest 属性,当用户点击删除时,调用 delete() 方法,这里的 this.hero 就是要传递给父组件的对象。现在看看父组件是如何绑定:

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

deleteRequest 触发时,Angular会调用父组件的 deleteHero 方法,这时 $evnet 的值就是前面的 this.hero 对象。

九、双向绑定

在开发表单时,通常是显示和更新并存。 [(ngModel)] 是双向绑定最简单语法,比如:

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

注:Angular把[()]叫做【香蕉在箱子里】,的确看起来像极了

另外还可以使用 bindon- 前缀:

<input bindon-ngModel="currentHero.firstName">

这语法有一个故事……

[(ngModel)]故事

刚开始,使用单独绑定 <input> 元素的 value 属性和 input 事件:

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

这太麻烦了,谁能记得住每一个元素要设置的属性和监听的事件?如何获取和更新文本框值?看到都烦。

后来用一个 ngModel 指令来隐藏这些繁锁难记的细节,通过指令的 ngModel 输入属性来设置元素值属性和 ngModelChange 输出属性来监听用户有的变更值。

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

可是 NgModel 实现细节只能支持一些文本框之类的元素;如果要支持自定义组件,还得自己写一个value accessor的东西。

还能改进吗?我们可不可不绑定两次属性,Angular应该能够用只用一个声明来捕获和设置组件数据属性。它就是 [()]【香蕉在箱子里】语法:

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

[(ngModel)] 实际只是一个语法糖,比如 [(x)] 语法会变成一个 x 输入属性来设置元素值属性和 xChange 输出属性来监听用户有的变更值。

[(x)]="hero.name" <==> [x]="hero.name" (xChange)="hero.name=$event"

[(ngModel)] 的确是我们需要的,那有没有一个理由能够让我们回到使用两次绑定的模式呢?

[()] 语法只能设置数据绑定属性,如果我们想要对值对扩展呢,比如让所有输入都是大写,此时用两次绑定更合适:

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

所有的效果如下:

ng-model-anim

十、内置指令

Angular1超过70个内置指令,社区贡献更多。

现在我们不需要这么多指令,使用更有表现力的Angular2绑定系统也能得到相同的结果。我们为什么要创建一个指令来处理点击呢?完全可以写一对括号和DOM元素简单的结合体,如:

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

下面介绍其它一些最常用的内置指令。

1、NgClass

我们通常会动态控制CSS类名,与之前的 [class.class-name] 相比,它可以控制多个CSS类名。

一个最好应用是通过 NgClass 绑定一个键值对,每个键表示类名,值的 true 表示可以添加、 false 表示可以移除,如果有的话。

setClasses() {
  let classes =  {
    saveable: this.canSave,      // true
    modified: !this.isUnchanged, // false
    special: this.isSpecial,     // true
  }
  return classes;
}

现在我们可以添加一个 NgClass 属性绑定到 setClasses

<div [ngClass]="setClasses()">This div is saveable and special</div>

2、NgStyle

我们可以根据组件状态,来动态设置内联样式。通过绑定 NgStyle 来管理多个内联样式。

一个最好应用是通过 NgStyle 绑定一个键值对,每个键表示样式名,值就是具体的样式值。和 [style.style-name] 不同的时,这里没有单位的说明。

setStyles() {
  let styles = {
    // CSS property names
    'font-style':  this.canSave      ? 'italic' : 'normal',  // italic
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',  // normal
    'font-size':   this.isSpecial    ? '24px'   : '8px',     // 24px
  }
  return styles;
}

现在我们可以添加一个 NgStyle 属性绑定到 setStyles

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

3、NgIf

通过绑定 NgIf 指令来添加或移除DOM元素。(注:这里是一个truthy表达式,什么意思呢?truthy是JavaScript字面意思,对于 falseundefinednullNaN0""都属于 false

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

不要忘记 *ngIf 前面的星号,见【十一、关于模板中*号】

当绑定到一个结果为 falsey 表达式时移除DOM元素。

<!-- not displayed because nullHero is falsey.
    `nullHero.firstName` never has a chance to fail -->
<div *ngIf="nullHero">Hello, {{nullHero.firstName}}</div>

<!-- Hero Detail is not in the DOM because isActive is false-->
<hero-detail *ngIf="isActive"></hero-detail>

元素的可见度和NgIf区别

我们可以使用绑定类或内联样式来显示或隐藏DON元素。

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

隐藏DOM元素跟使用 NgIf 移除DOM元素完全不同。

当我们隐藏DOM元素时,它还保留在DOM里面,其下面的组件状态也一并存在;Angular依然会继续检查并占用内存和计算资源,只是我们看不到而已。

NgIffalse 时,Angular实际上是移除元素,并销毁其下所有组件。

4、NgSwitch

使用 NgSwitch 指令来显示多个元素中的一个。

<span [ngSwitch]="toeChoice">
  <span *ngSwitchWhen="'Eenie'">Eenie</span>
  <span *ngSwitchWhen="'Meanie'">Meanie</span>
  <span *ngSwitchWhen="'Miney'">Miney</span>
  <span *ngSwitchWhen="'Moe'">Moe</span>
  <span *ngSwitchDefault>other</span>
</span>

我们在父 <span> 绑定了 NgSwitch 指令,表达式返回 switch 值,示例中值为字符串,其实值可以是任意类型。。

在这个示例,父 NgSwith 指令控制一组 <span> 元素,这些 <span> 要么是用于匹配表达式、要么是默认值。

整个过程如同我们JavaScript的 switch 一样,匹配到就把该 <span> 加入到DOM里,如果未匹配到则使用默认的,如果连默认都没有,那就什么都没有。

NgSwitch 需要三种指令协同工作,ngSwitchngSwitchWhenngSwitctDefault 三个指令如同 JavaScript switch 语法一样。

注意:ngSwitch 他是一个属性绑定所以不用 * 号。而 ngSwitchWhenngSwitctDefault 要加星号,见【十一、关于模板中*号】。

5、NgFor

NgFor 是一个循环指令,是一种自定义数据显示的方法。

目标是实现一个列表,用一段HTML片段来定义单个列表项,最后告诉Angular用段HTML作为模板来渲染列表中的每一项。例如:

<div *ngFor="#hero of heroes">{{hero.fullName}}</div>

或者应用到组件元素中:

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

不要忘记 *ngFor 前面的星号,见【十一、关于模板中*号】

NgFor微语法

赋值给 *ngFor 不是表达式,而是微语法(microsyntax)——一种Angular解析器自带的语言。示例中 #hero of heroes 的意思是:

heroes 数组中的每一项存储至本地变量 hero 里,且每次迭代时其HTML片断都能访问该变量。

在前面两个示例中,ngFor 指令遍历父组件类 heroes 属性返回的 heroes 数组,Angular会为每项创建一个 hero 实例并存储在本地模板变量中,在模板内就可以访问该属性或把变量传递给组件元素。

NgFor索引

ngFor 指令支持一个可选项 index,其值从0开始增加,我们也可以把它保存到本地模板变量中。比如:

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

还包括其他特殊索引值,比如:lastevenodd,详细见 NgFor API参考

NgForTrackBy

ngFor 指令在大列表面前可能表现不佳,可能会因为一个项的改动,引起一连串的DOM操作。例如:通过重新查询服务器来刷新 heroes 列表,但是可能多数是先前显示的。虽然我们可以根据 id 判断每个 hero 是否有改变,但对于Angular而言只看到引用了新数组,它会先移除再重新插入元素。

可以使用一个跟踪函数来告诉Angular说,我们两个是一样的,没有变动过,你就不要变动我的DOM了。比如我们用 hero.id 来判断是否相同的 hreo。

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

现在设置 NgForTrackBy 指令来调用这个跟踪函数,Angular提供了两种语法选择:

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

跟踪函数并不能消除所有DOM的变化,如果相同的 hero 其属性变化DOM也会更着变化;如果属性没有改变,大部分时间都不会改变。

这儿有个 NgForTrackBy 效果的插图:

ng-for-track-by-anim

十一、关于模板中*号

当我们回顾 NgForNgIfNgSwitch 内置指令时,用了一个很奇怪语法,在指令前加 * 星号。

* 是个语法糖,让指令更容易读写,且有助于HTML布局,NgForNgIfNgSwitch 指令添加和移除的元素都包装在一个 <template> 标签中,但我们并没有看它,因为 * 语法让我们可以忽略它。

下面我们揭开来看看,Angular去除 * 号后是如何使用 <template> 标签。

展开*ngIf

我们可以将 * 前缀语法展开成模板语法。例如使用 *ngIf 时:

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

currentHero 引用了两次,为NgIf提供布尔条件,其次是把hero传递给 HeroDetailComponent

先将 ngIf (无 * 前缀)和内容转换成表达式,并赋值给 template 指令:

<hero-detail template="ngIf:currentHero" [hero]="currentHero"></hero-detail>

最后,扩展一个 <template> 标签并设置 [ngIf] 属性绑定。

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

展开*ngSwitch

*ngSwtich 的转换也很类似,下面示例是先使用 *ngSwitchWhen*ngSwitchDefault,然后再使用 <template> 标签:

<span [ngSwitch]="toeChoice">

    <!-- with *NgSwitch -->
    <span *ngSwitchWhen="'Eenie'">Eenie</span>
    <span *ngSwitchWhen="'Meanie'">Meanie</span>
    <span *ngSwitchWhen="'Miney'">Miney</span>
    <span *ngSwitchWhen="'Moe'">Moe</span>
    <span *ngSwitchDefault>other</span>

    <!-- with <template> -->
    <template [ngSwitchWhen]="'Eenie'"><span>Eenie</span></template>
    <template [ngSwitchWhen]="'Meanie'"><span>Meanie</span></template>
    <template [ngSwitchWhen]="'Miney'"><span>Miney</span></template>
    <template [ngSwitchWhen]="'Moe'"><span>Moe</span></template>
    <template ngSwitchDefault><span>other</span></template>

</span>

*ngSwitchWhen*ngSwitchDefault 的展开后和 *ngIf 完全相同方式,利用 <template> 标签来包裹之前的元素。

现在我们可以看到为什么 ngSwitch 它自己不带星号前缀了,它没有定义任何内容,只是用来管理下面模板集合。

展开*ngFor

*ngFor 的转换也很类似,最初写法:

<hero-detail *ngFor="#hero of heroes; trackBy:trackByHeroes" [hero]="hero"></hero-detail>

ngFor 转换成 template 指令后:

<hero-detail template="ngFor #hero of heroes; trackBy:trackByHeroes" [hero]="hero"></hero-detail>

最后,使用一个 <template> 标签包裹着原来的 <hero-detail> 元素:

<template ngFor #hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
  <hero-detail [hero]="hero"></hero-detail>
</template>

十二、本地模板变量

本地模板变量(local template variable)用于元素间数据传递。

前几次都可在 NgFor 循环时就用到用 #hero 定义本地变量。而在【十一、关于模板中*号】学习了如果把 *ngFor 展开出使用 <template> 来表述,例如:

<template ngFor #hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
  <hero-detail [hero]="hero"></hero-detail>
</template>

这里带有 # 前缀 “hero” 就是相当于定义了 hero 变量。你也可以用 ver- 前缀,比如 ver-hero

引用本地模板变量

本地模板变量只能在当前元素、兄弟元素、任何子元素中被引用。

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

<!-- fax refers to the input element; pass its `value` to an event handler -->
<input var-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>

如何确认变量值

本地模板变量的值取决于所处位置的上下文,比如上面的 #phone 其值就是 input DOM对象,再前面一点的示例中是运用在 NgFor 中,其值是循环数组中的项。

NgForm与本地模板变量

先看一段表单模板:

<form (ngSubmit)="onSubmit(theForm)" #theForm="ngForm">
  <div class="form-group">
    <label for="name">Name</label>
    <input class="form-control" required ngControl="firstName"
      [(ngModel)]="currentHero.firstName">
  </div>
  <button type="submit" [disabled]="!theForm.form.valid">Submit</button>
</form>

本地模板变量 theForm 共出现了三次,那它的值到底是啥?

如果没有Angular,那它就是 HTMLFormElement。而在Angular里,他是指 ngForm 引用的是内置指令 NgForm(因为Angular默认会给所有form应用NgForm指令);实际就是在 HTMLFormElement 的基础上加一些扩展,比如:跟踪用户录入有效性。这也就是解释了为什么用 theForm.form.valid 来检查提交按钮的可用状态。

十三、输入和输出属性

到目前为止,我们主要集中在 = 右边的内容,也就是数据绑定部分。本节讨论绑定的目标部分(也就是 = 左边的内容)。所以我把数据绑定的目标= 来划清他们。(记住:所有组件都是指令)

目标:由这三种绑定符号 []()[()],包裹着属性或事件;你可以通过显示标识 inputsoutpus 来表明绑定限制。
:用引号 "" 或 插值绑定 {{}},包裹着内容。

重点来看 HeroDetailComponent 目标绑定部分:

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

HeroDetailComponent.heroHeroDetailComponent.deleteRequest 都是目标绑定,前者是中括号包裹用于属性绑定;后者是括号包裹用于事件绑定。

如前所述:属性绑定的数据流从组件到元素;事件绑定的数据流从元素到组件,所以看得出他们两种风格代表着输出和输入。

定义输入和输入属性

目标属性必须显示标识到底是输入或输出。回看 HeroDetailComponent 可以看到,通过装饰器来标识输入和输出属性:

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

另外,也可以在指令元数据定义 inputsoutputs 数组。

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

这两种方式只能选一种。

输入/输出属性的别名

有时我们希望输入/输出属性对外的名称和内部名称不一样。比如上面第一示例可以改写成:

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

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

十四、模板表达式运算符

模板表达式语言扩充一些JavaScript语法来解决特殊场景,包括:管道(pipe)和非空即真(Elvis)。

1、管道(pipe)

有时我们需要对绑定的结果做格式转换,比如:货币、大小写、过滤或排序列表等等。Angular管道是一个接受输入返回输出的简单函数,管道的操作号 | 很容易在模板表达中使用。比:

<!-- 将title转换成大写 -->
<div>{{ title | uppercase }}</div>

管道会将表达式的结果传递给右边的 uppercase 函数,而函数返回一个新的结果。

一个表达式支持多个管道:

<!-- 很作的示例:先转换成大写,再转换成小写,最终结果是:小写 -->
<div>{{ title | uppercase | lowercase }}</div>

当然,管道的函数也支持额外参数,函数名和参数用 : 隔开,比如以下指明格式化日期的格式类型为 longDate

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

json 管道对于调试时特别有帮助:

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

<!-- Output:
  { "firstName": "Hercules", "lastName": "Son of Zeus",
    "birthdate": "1970-02-25T08:00:00.000Z",
    "url": "http://www.imdb.com/title/tt0065832/",
    "rate": 325, "id": 1 }
-->

2、非空即真(Elvis)

另一个好用的语法糖,不过当我们不确认某个对象是否可能出现为 null 我们依然在表达式引用其下的属性。

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

这样会抛出异常,没有用语法糖前我们可能会这么做:

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

换成语法糖只需要这样:

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

世界一下就安静了!

十六、总结

整体我感觉模板语法比1.x更简单,数据流方向更明确。

Angular2的绑定系统减少大量内置指令,转而根据大家所知的HTML元素的基础上做解析,极大地简化认知成本。

关于数据流方向相信很多人在1.x时都有过坑,你无法很明确了解数据流方向倒置很多时候拿到的数据不一定是最新的。而2里面把它们分成三种不同的语法来表示数据流方向,这样虽然无法彻底清楚方向,但是我们可以有办法去控制它。当然如果结合Redux无疑是最优的解决方案。