指令概览
在 Angular 中有三种类型的指令:
-
-
组件 — 拥有模板的指令
-
结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令
-
属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。
-
属性型指令改变一个元素的外观或行为。例如,内置的 NgStyle 指令可以同时修改元素的多个样式
简单的属性型指令
属性型指令至少需要一个带有@Directive
装饰器的控制器类
<p appHightlight>Highlight me!</p>
import { Directive, ElementRef, Input } from '@angular/core';
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = 'yellow';
}
}
确认你没有给指令添加ng
前缀。 那个前缀属于 Angular,使用它可能导致难以诊断的 bug。例如,这个简短的前缀my
可以帮助你区分自定义指令。
ElementRef
是一个服务,它赋予我们通过它的nativeElement
属性直接访问 DOM 元素的能力
把这个类添加到 NgModule 元数据的declarations
数组中。 这样,当 Angular 在模板中遇到myHighlight
时,就能认出这是指令了
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HighlightDirective } from './highlight.directive';
@NgModule({
imports: [ BrowserModule ],
declarations: [
AppComponent,
HighlightDirective
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
总结:Angular 在<p>
元素上发现了一个myHighlight
属性。 然后它创建了一个HighlightDirective
类的实例,并把所在元素的引用注入到了指令的构造函数中。 在构造函数中,我们把<p>
元素的背景设置为了黄色。
你的指令没生效?
你记着设置@NgModule
的declarations
数组了吗?它很容易被忘掉。
响应用户引发的事件
myHighlight
只是简单的设置元素的颜色。 这个指令应该在用户鼠标悬浮一个元素时,设置它的颜色
- 把
HostListener
加进导入列表中,同时再添加Input
符号 (@HostListener
装饰器引用属性型指令的宿主元素)import { Directive, ElementRef, HostListener, Input } from '@angular/core'; @HostListener('mouseenter') onMouseEnter() { this.highlight('yellow'); } @HostListener('mouseleave') onMouseLeave() { this.highlight(null); } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; }
(当然,你可以通过标准的JavaScript方式手动给宿主 DOM 元素附加一个事件监听器。 但这种方法至少有三个问题:
-
-
必须正确的书写事件监听器。
-
当指令被销毁的时候,必须拆卸事件监听器,否则会导致内存泄露。
-
必须直接和 DOM API 打交道,应该避免这样做。)
-
使用数据绑定向指令传递值
让指令的使用者可以在模板中通过绑定来设置颜色。
我们先把highlightColor
属性添加到指令类中,就像这样
@Input() highlightColor: string;
使用:
<p appHightlight highlightColor="yellow">Highlighted in yellow</p>
<p appHightlight [highlightColor]="'orange'">Highlighted in orange</p>
我们可以在应用该指令时在同一个属性中设置颜色
<p [myHighlight]="color">Highlight me!</p>
//@Input() myHighlight: string;
//绑定到@Input别名
@Input('myHighlight') highlightColor: string;
最终版:
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) { }
@Input('appHighlight') highlightColor: string;
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || 'red');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
绑定到第二个属性
@Input() defaultColor: string;
<p [appHighlight]="color" defaultColor="violet">
Angular之所以知道defaultColor
绑定属于HighlightDirective
,是因为我们已经通过@Input
装饰器把它设置成了公共属性
结构型指令:
*ngIf , *ngFor 等
每个宿主元素上只能有一个结构型指令
有时我们会希望只有当特定的条件为真时才重复渲染一个 HTML 块。 你可能试过把*ngFor
和*ngIf
放在同一个宿主元素上,但Angular 不允许。这是因为我们在一个元素上只能放一个结构型指令。
原因很简单。结构型指令可能会对宿主元素及其子元素做很复杂的事。当两个指令放在同一个元素上时,谁先谁后?NgIf
优先还是NgFor
优先?NgIf
可以取消NgFor
的效果吗? 如果要这样做,Angular 应该如何把这种能力泛化,以取消其它结构型指令的效果呢?
对这些问题,没有办法简单回答。而禁止多个结构型指令则可以简单地解决这个问题。 这种情况下有一个简单的解决方案:把*ngIf
放在一个"容器"元素上,再包装进 *ngFor
元素。 这个元素可以使用ng-container
,以免引入一个新的HTML层级。
NgSwitch
内幕:
Angular 的 NgSwitch
实际上是一组相互合作的指令:NgSwitch
、NgSwitchCase
和 NgSwitchDefault
。
例子如下:
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
一个值(hero.emotion
)被被赋值给了NgSwitch
,以决定要显示哪一个分支。
NgSwitch
本身不是结构型指令,而是一个属性型指令,它控制其它两个switch指令的行为。 这也就是为什么我们要写成[ngSwitch]
而不是*ngSwitch
的原因。
NgSwitchCase
和 NgSwitchDefault
都是结构型指令。 因此我们要使用星号(*
)前缀来把它们附着到元素上。NgSwitchCase
会在它的值匹配上选项值的时候显示它的宿主元素。 NgSwitchDefault
则会当没有兄弟NgSwitchCase
匹配上时显示它的宿主元素。
像其它的结构型指令一样,NgSwitchCase
和 NgSwitchDefault
也可以解开语法糖,变成 <ng-template>
的形式
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<app-happy-hero [hero]="hero"></app-happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<app-sad-hero [hero]="hero"></app-sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<app-confused-hero [hero]="hero"></app-confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<app-unknown-hero [hero]="hero"></app-unknown-hero>
</ng-template>
</div>
优先使用星号(*
)语法
星号(*
)语法比不带语法糖的形式更加清晰。 如果找不到单一的元素来应用该指令,可以使用<ng-container>作为该指令的容器。
虽然很少有理由在模板中使用结构型指令的属性形式和元素形式,但这些幕后知识仍然是很重要的,即:Angular会创建<ng-template>
,还要了解它的工作原理。 当需要写自己的结构型指令时,我们就要使用<ng-template>
<ng-template>指令
<ng-template>是一个 Angular 元素,用来渲染HTML。 它永远不会直接显示出来。 事实上,在渲染视图之前,Angular 会把<ng-template>
及其内容替换为一个注释。
如果没有使用结构型指令,而仅仅把一些别的元素包装进<ng-template>
中,那些元素就是不可见的。 在下面的这个短语"Hip! Hip! Hooray!"中,中间的这个 "Hip!"(欢呼声) 就是如此。
<p>Hip!</p> <ng-template> <p>Hip!</p> </ng-template> <p>Hooray!</p>
使用<ng-container>把一些兄弟元素归为一组
通常都要有一个根元素作为结构型指令的数组。 列表元素(<li>
)就是一个典型的供NgFor
使用的宿主元素
<li *ngFor="let hero of heroes">{{hero.name}}</li>
当没有这样一个单一的宿主元素时,我们可以把这些内容包裹在一个原生的HTML容器元素中,比如<div>
,并且把结构型指令附加到这个"包裹"上。
<div *ngIf="hero" >{{hero.name}}</div>
这样子,通常也会带来问题。注意,是"通常"而不是"总会"。
问题1:样式
p span { color: red; font-size: 70%; }
<span *ngIf="hero"> and saw {{hero.name}}. I waved </span>(本来样式不应该应用到这里,意外引入进来)
另一个问题是:有些HTML元素需要所有的直属下级都具有特定的类型。 比如,<select>
元素要求直属下级必须为<option>
,那么我们就没办法把这些选项包装进<div>
或<span>
中。
<select [(ngModel)]="hero">
<span *ngFor="let h of heroes">
<span *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</span>
</span>
</select>
下拉列表就是空的
<ng-container> 的救赎
<ng-container>
是一个由 Angular 解析器负责识别处理的语法元素。 它不是一个指令、组件、类或接口,更像是 JavaScript 中 if
块中的花括号。
<select [(ngModel)]="hero">
<ng-container *ngFor="let h of heroes">
<ng-container *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</ng-container>
</ng-container>
</select>
写一个结构型指令
写一个名叫UnlessDirective
的结构型指令,它是NgIf
的反义词。 NgIf
在条件为true
的时候显示模板内容,而UnlessDirective
则会在条件为false
时显示模板内容
创建指令很像创建组件。
-
-
导入符号
Input
、TemplateRef
和ViewContainerRef
,我们在任何结构型指令中都会需要它们。 -
给指令类添加装饰器。
-
设置 CSS 属性选择器 ,以便在模板中标识出这个指令该应用于哪个元素
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
TemplateRef 和 ViewContainerRef
这个例子一样的简单结构型指令会从 Angular 生成的<ng-template>
元素中创建一个内嵌的视图,并把这个视图插入到一个视图容器中,紧挨着本指令原来的宿主元素<p>
我们可以使用TemplateRef
取得<ng-template>
的内容,并通过ViewContainerRef
来访问这个视图容器
一旦 appUnless 该值的条件发生了变化,Angular 就会去设置 myUnless
属性,这时候,我们就需要为它定义一个设置器(setter)。
-
-
如果条件为假,并且以前尚未创建过该视图,就告诉视图容器(ViewContainer)根据模板创建一个内嵌视图。
-
如果条件为真,并且视图已经显示出来了,就会清除该容器,并销毁该视图。
-