NgModel属性用于双向数据绑定,既显示数据属性又接受用户的改变:
<input [(ngModel)]="currentHero.firstName">
也可以使用前缀方式:
<input bindon-ngModel="currentHero.firstName">
我们看一看关于双向数据绑定的发展历程。之前提到过,可以使用<input>
元素的value
属性和input
事件实现跟NgModel
相同的效果:
<input [value]="currentHero.firstName"
(input)="currentHero.firstName=$event.target.value" >
这样子太繁琐了。我们需要记住元素要设置的属性和监听的事件。ngModel
指令隐藏了这些细节。ngModel
包装了元素的value
属性,监听input
事件,并且提供了自己的事件。
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="currentHero.firstName=$event">
这个还有改进的地方。为什么我们要绑定两次呢?Angular应该能做到用一个声明来读取和设置组件书数据属性,那就是[()]
:
<input [(ngModel)]="currentHero.firstName">
在内部,Angular会将ngModel
映射为ngModel
输入属性和ngModelChange
输出属性。
当然,在一些场景下还是有理由将[(ngModel)]
拆成两个绑定的。例如,强制将用户的输入转换成大写:
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="setUpperCaseFirstName($event)">
下面是一些常见的内建指令。
我们通常通过添加或移除CSS类来控制元素的外观。我们可以绑定NgClass
来同时添加或移除多个CSS类。
之前提到过使用class绑定来添加或移除单个CSS类:
<!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>
而NgClass
指令可以一次添加或移除多个CSS类。使用NgClass
的方式是为它绑定一个key-value对象。对象的key是CSS类名;value为true时添加该类,为false时移除该类。
假设组件提供了setClasses
方法管理CSS类的状态:
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>
之前提到过使用style绑定来添加或移除单个样式:
<div [style.fontSize]="isSpecial ? 'x-large' : 'smaller'" >
This div is x-large
</div>
而NgStyle
指令可以一次添加或移除多个内联样式。使用NgStyle
的方式是为它绑定一个key-value对象。对象的key是样式名称;value就是样式的值。
假设组件提供了setStyles
方法管理样式:
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>
NgIf指令可以控制是否想DOM添加或移除一个元素。绑定的表达式值为真值时添加元素,为假值时移除元素。
<div *ngIf="currentHero">Hello, {{currentHero.firstName}}</div>
<!-- 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>
我们可以也使用class绑定或style绑定控制显示或隐藏DOM元素:
<!-- 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中。相应的组件都是存在的,保留着它们的状态,仍然消耗着内存,Angular可能会继续检查它的属性的变化。而NgIf
值为false时,Angular会将元素中DOM中移除,释放掉组件,不再占用资源。
我们使用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>
注意,ngSwitch
用的是属性绑定,不用加*
,而ngSwitchWhen
和ngSwitchDefault
前面是加了*
的。
NgFor
是一个repeater指令。
我们的目标是现实一个列表,我们用一个HTML片段定义单个列表项如何显示,然后告诉Angular用这个HTML片段作为模板来渲染列表中的每一项。
例如,对<div>
使用NgFor
:
<div *ngFor="#hero of heroes">{{hero.fullName}}</div>
也可以对组件元素使用NgFor
:
<hero-detail *ngFor="#hero of heroes" [hero]="hero"></hero-detail>
赋值给*ngFor
的字符串不是模板表达式,而是一个微语法(microsyntax)。例子中的#hero of heroes
的意思是:对heroes
数组中的每一项,将它赋值给本地变量hero
,hero
在每次迭代的HTML中可访问。Angular会将该绑定转换成一系列的元素和绑定。
ngFor
指令有个索引index
,从0开始。我们可以用一个本地变量来捕获索引并在模板中使用。
<div *ngFor="#hero of heroes; #i=index">{{i + 1}} - {{hero.fullName}}</div>
还有一些值如last
、even
、odd
。
NgFor
指令在列表很大时性能可能并不是很好。例如,我们从服务器更新heroes数组,新的数组中可能大部分都是之前显示的,我们可以根据id来判断每个hero是否改变。但是Angular只会看到新的数组引用,它会移除掉之前的所有元素,根据新数组重新构建并插入元素。
我们可以提供一个tracking函数来告诉Angular如何识别数组项是否一样:具有相同的hero.id
的hero是相同的。
trackByHeroes(index: number, hero: Hero) { return hero.id; }
设置NgForTrackBy
指令为定义的tracking函数,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>
*
和<template>
我们注意到在使用NgFor
、NgIf
、NgSwitch
时,用到了一个很奇怪的语法,在指令名前加前缀*
。
*
是个语法糖,让指令的阅读和编写更加简单。NgFor
、NgIf
和NgSwitch
指令添加和移除的元素都包装在<template>
中。但是我们并没有看到<template>
,因为*
前缀语法让我们可以忽略它,从而更多地关注我们要添加、移除或者重复的HTML元素。
*ngIf
我们可以将*
前缀语法展开成模板语法。例如使用*ngIf
时:
<hero-detail *ngIf="currentHero" [hero]="currentHero"></hero-detail>
第一步,将ngIf(没有星号的)
和它的内容转换成分配给template
指令的表达式:
<hero-detail template="ngIf:currentHero" [hero]="currentHero"></hero-detail>
第二步(也是最后一步),将HTML 放到<template>
标签内,并添加ngIf
属性绑定:
<template [ngIf]="currentHero">
<hero-detail [hero]="currentHero"></hero-detail>
</template>
*ngSwitch
*ngSwitch
的转换也是类似的:
<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>
*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>
元素:
<template ngFor #hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
<hero-detail [hero]="hero"></hero-detail>
</template>
本地模板变量用于在元素之间传递数据。例如使用<template>
的ngFor
中:
<template ngFor #hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
<hero-detail [hero]="hero"></hero-detail>
</template>
用#
前缀hero意味着我们定义了一个变量hero
。也可以使用var-
前缀。
我们可以在同一个元素、兄弟元素、子元素中使用本地模板变量。
<!-- 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>
看这个HTML表单:
<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
,Angular内建指令NgForm
的引用,它包装了HTMLFormElement,并提供了一些额外的特性。
之前我们关注的都是绑定声明右侧的模板表达式和模板声明,也就是数据绑定源。接下来再看一下绑定目标,绑定声明左侧的指令属性。这些指令属性必须声明为输入还是输出。
绑定目标就是绑定符号([]
、()
、[()]
)内的属性或事件,绑定源就是引号(""
)或这插值绑定({}
)内的内容。
只有显式标识为inputs或outputs的属性才能作为绑定目标。
例如HeroDetailComponent
中:
<hero-detail [hero]="currentHero" (deleteRequest)="deleteHero($event)">
</hero-detail>
HeroDetailComponent.hero
和HeroDetailComponent.deleteRequest
都是绑定目标。HeroDetailComponent.hero
在小括号内,它是个属性绑定的目标;HeroDetailComponent.deleteRequest
在中括号内,它是个事件绑定的目标。
目标属性必须被显式地声明为输入或者输出:
@Input() hero: Hero;
@Output() deleteRequest = new EventEmitter<Hero>();
或者在指令元数据的inputs
和outputs
数组中指定:
@Component({
inputs: ['hero'],
outputs: ['deleteRequest'],
})
有时候我们希望为输入/输出属性提供一个对外的名称。
例如,我们想绑定一个事件属性myClick
:
<div (myClick)="clickMessage=$event">click with myClick</div>
将组件的clicks
事件属性起个别名myClick
。
@Output('myClick') clicks = new EventEmitter<string>(); // @Output(alias) propertyName = ...
或者实在元数据中指定:
@Directive({
outputs:['clicks:myClick'] // propertyName:alias
})
模板表达式扩展了一些JavaScript没有的操作符: pipe和Elvis。
有时候表达式的值在绑定前需要做一些转换。例如,将数字转换成货币格式、将字符串转成大写、对数组进行过滤或者排序。
管道(Pipe)个简单的函数,接收出入值,返回转换后的值。例如:
<!-- Force title to uppercase -->
<div>{{ title | uppercase }}</div>
管道运算符将左侧表达式的值传递给右侧的管道函数。
我们可以将多个管道函数链接起来:
<!-- Pipe chaining: force title to uppercase, then to lowercase -->
<div>{{ title | uppercase | lowercase }}</div>
也可以对管道函数进行配置:
<!-- 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 }
-->
这个操作符是为了保证属性路径中有null或undefined值时不会出错:
The current hero's name is {{currentHero?.firstName}}
这样,即使currentHero
为null,Angular也不会报错了。
参考资料
Angular官方文档