屬性型指令用于改變一個(gè) DOM 元素的外觀或行為。
在 Angular 中有三種類型的指令:
組件是這三種指令中最常用的。 你在快速上手例子中第一次見到組件。
結(jié)構(gòu)型指令修改視圖的結(jié)構(gòu)。例如,NgFor
和 NgIf
。 要了解更多,參見結(jié)構(gòu)型指令 指南。
屬性型指令改變一個(gè)元素的外觀或行為。例如,內(nèi)置的 NgStyle
指令可以同時(shí)修改元素的多個(gè)樣式。
屬性型指令至少需要一個(gè)帶有 @Directive
裝飾器的控制器類。該裝飾器指定了一個(gè)用于標(biāo)識(shí)屬性的選擇器。 控制器類實(shí)現(xiàn)了指令需要的指令行為。
本章展示了如何創(chuàng)建一個(gè)簡(jiǎn)單的屬性型指令 appHighlight
,當(dāng)用戶把鼠標(biāo)懸停在一個(gè)元素上時(shí),改變它的背景色。你可以這樣用它:
Path:"src/app/app.component.html (applied)"
<p appHighlight>Highlight me!</p>
注:
- 指令不支持命名空間。
在命令行窗口下用 CLI
命令 ng generate directive
創(chuàng)建指令類文件。
ng generate directive highlight
CLI
會(huì)創(chuàng)建 "src/app/highlight.directive.ts" 及相應(yīng)的測(cè)試文件("src/app/highlight.directive.spec.ts"),并且在根模塊 AppModule
中聲明這個(gè)指令類。
注:
- 和組件一樣,這些指令也必須在Angular 模塊中進(jìn)行聲明。
生成的 "src/app/highlight.directive.ts" 文件如下:
Path:"src/app/highlight.directive.ts"
import { Directive } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor() { }
}
這里導(dǎo)入的 Directive
符號(hào)提供了 Angular 的 @Directive
裝飾器。
@Directive
裝飾器的配置屬性中指定了該指令的 CSS 屬性型選擇器 [appHighlight]
這里的方括號(hào)([]
)表示它的屬性型選擇器。 Angular 會(huì)在模板中定位每個(gè)擁有名叫 appHighlight
屬性的元素,并且為這些元素加上本指令的邏輯。
正因如此,這類指令被稱為 屬性選擇器。
緊跟在 @Directive
元數(shù)據(jù)之后的就是該指令的控制器類,名叫 HighlightDirective
,它包含了該指令的邏輯(目前為空邏輯)。然后導(dǎo)出 HighlightDirective
,以便它能在別處訪問到。
現(xiàn)在,把剛才生成的 "src/app/highlight.directive.ts" 編輯成這樣:
Path:"src/app/highlight.directive.ts"
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = 'yellow';
}
}
import
語(yǔ)句還從 Angular 的 core
庫(kù)中導(dǎo)入了一個(gè) ElementRef
符號(hào)。
你可以在指令的構(gòu)造函數(shù)中使用 ElementRef
來注入宿主 DOM 元素的引用,也就是你放置 appHighlight
的那個(gè)元素。
ElementRef
通過其 nativeElement
屬性給你了直接訪問宿主 DOM 元素的能力。
這里的第一個(gè)實(shí)現(xiàn)把宿主元素的背景色設(shè)置為了黃色。
要想使用這個(gè)新的 HighlightDirective
,就往根組件 AppComponent
的模板中添加一個(gè) <p>
元素,并把該指令作為一個(gè)屬性使用。
Path:"src/app/app.component.html"
<p appHighlight>Highlight me!</p>
運(yùn)行這個(gè)應(yīng)用以查看 HighlightDirective
的實(shí)際效果。
ng serve
總結(jié):Angular 在宿主元素 <p>
上發(fā)現(xiàn)了一個(gè) appHighlight
屬性。 然后它創(chuàng)建了一個(gè) HighlightDirective
類的實(shí)例,并把所在元素的引用注入到了指令的構(gòu)造函數(shù)中。 在構(gòu)造函數(shù)中,該指令把 <p>
元素的背景設(shè)置為了黃色。
當(dāng)前,appHighlight
只是簡(jiǎn)單的設(shè)置元素的顏色。 這個(gè)指令應(yīng)該在用戶鼠標(biāo)懸浮一個(gè)元素時(shí),設(shè)置它的顏色。
先把 HostListener
加進(jìn)導(dǎo)入列表中。
Path:"src/app/highlight.directive.ts (imports)"
import { Directive, ElementRef, HostListener } from '@angular/core';
然后使用 HostListener
裝飾器添加兩個(gè)事件處理器,它們會(huì)在鼠標(biāo)進(jìn)入或離開時(shí)進(jìn)行響應(yīng)。
Path:"src/app/highlight.directive.ts (mouse-methods)"
@HostListener('mouseenter') onMouseEnter() {
this.highlight('yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
@HostListener
裝飾器讓你訂閱某個(gè)屬性型指令所在的宿主 DOM 元素的事件,在這個(gè)例子中就是 <p>
。
當(dāng)然,你可以通過標(biāo)準(zhǔn)的 JavaScript 方式手動(dòng)給宿主 DOM 元素附加一個(gè)事件監(jiān)聽器。 但這種方法至少有三個(gè)問題:
- 必須正確的書寫事件監(jiān)聽器。
- 當(dāng)指令被銷毀的時(shí)候,必須拆卸事件監(jiān)聽器,否則會(huì)導(dǎo)致內(nèi)存泄露。
- 必須直接和 DOM API 打交道,應(yīng)該避免這樣做。
這些處理器委托了一個(gè)輔助方法來為 DOM 元素(el
)設(shè)置顏色。
這個(gè)輔助方法(highlight
)被從構(gòu)造函數(shù)中提取了出來。 修改后的構(gòu)造函數(shù)只負(fù)責(zé)聲明要注入的元素 el: ElementRef
。
Path:"src/app/highlight.directive.ts (constructor)"
constructor(private el: ElementRef) { }
下面是修改后的指令代碼:
Path:"src/app/highlight.directive.ts"
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) { }
@HostListener('mouseenter') onMouseEnter() {
this.highlight('yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
運(yùn)行本應(yīng)用并確認(rèn):當(dāng)把鼠標(biāo)移到 p 上的時(shí)候,背景色就出現(xiàn)了,而移開時(shí)就消失了。
高亮的顏色目前是硬編碼在指令中的,這不夠靈活。 在這一節(jié)中,你應(yīng)該讓指令的使用者可以指定要用哪種顏色進(jìn)行高亮。
先從 @angular/core
中導(dǎo)入 Input
。
Path:"src/app/highlight.directive.ts (imports)"
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
然后把 highlightColor
屬性添加到指令類中,就像這樣:
Path:"src/app/highlight.directive.ts (highlightColor)"
@Input() highlightColor: string;
注意看 @Input
裝飾器。它往類上添加了一些元數(shù)據(jù),從而讓該指令的 highlightColor
能用于綁定。
它之所以稱為輸入屬性,是因?yàn)閿?shù)據(jù)流是從綁定表達(dá)式流向指令內(nèi)部的。 如果沒有這個(gè)元數(shù)據(jù),Angular 就會(huì)拒絕綁定,參見稍后了解更多。
試試把下列指令綁定變量添加到 AppComponent
的模板中:
Path:"src/app/app.component.html (excerpt)"
<p appHighlight highlightColor="yellow">Highlighted in yellow</p>
<p appHighlight [highlightColor]="'orange'">Highlighted in orange</p>
把 color
屬性添加到 AppComponent
中:
Path:"src/app/app.component.ts (class)"
export class AppComponent {
color = 'yellow';
}
讓它通過屬性綁定來控制高亮顏色。
Path:"src/app/app.component.html (excerpt)"
<p appHighlight [highlightColor]="color">Highlighted with parent component's color</p>
很不錯(cuò),但如果可以在應(yīng)用該指令時(shí)在同一個(gè)屬性中設(shè)置顏色就更好了,就像這樣:
Path:"src/app/app.component.html (color)"
<p [appHighlight]="color">Highlight me!</p>
[appHighlight]
屬性同時(shí)做了兩件事:把這個(gè)高亮指令應(yīng)用到了 <p>
元素上,并且通過屬性綁定設(shè)置了該指令的高亮顏色。 你復(fù)用了該指令的屬性選擇器 [appHighlight]
來同時(shí)完成它們。 這是清爽、簡(jiǎn)約的語(yǔ)法。
你還要把該指令的 highlightColor
改名為 appHighlight
,因?yàn)樗穷伾珜傩阅壳暗慕壎?/p>
Path:"src/app/highlight.directive.ts (renamed to match directive selector)"
@Input() appHighlight: string;
這可不好。因?yàn)?appHighlight
是一個(gè)糟糕的屬性名,而且不能反映該屬性的意圖。
幸運(yùn)的是,你可以隨意命名該指令的屬性,并且給它指定一個(gè)用于綁定的別名。
恢復(fù)原始屬性名,并在 @Input
的參數(shù)中把該選擇器指定為別名。
Path:"src/app/highlight.directive.ts (color property with alias)"
@Input('appHighlight') highlightColor: string;
在指令內(nèi)部,該屬性叫 highlightColor
,在外部,你綁定到它地方,它叫 appHighlight
。
這是最好的結(jié)果:理想的內(nèi)部屬性名,理想的綁定語(yǔ)法:
Path:"src/app/app.component.html (color)"
<p [appHighlight]="color">Highlight me!</p>
現(xiàn)在,你通過別名綁定到了 highlightColor
屬性,并修改 onMouseEnter()
方法來使用它。 如果有人忘了綁定到 appHighlight
,那就用紅色進(jìn)行高亮。
Path:"src/app/highlight.directive.ts (mouse enter)"
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || 'red');
}
這是最終版本的指令類。
Path:"src/app/highlight.directive.ts (excerpt)"
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;
}
}
憑空想象該指令如何工作可不容易。你需要把 AppComponent
改成一個(gè)測(cè)試程序,它讓你可以通過單選按鈕來選取高亮顏色,并且把你選取的顏色綁定到指令中。
把 "app.component.html" 修改成這樣:
Path:"src/app/app.component.html (v2)"
<h1>My First Attribute Directive</h1>
<h4>Pick a highlight color</h4>
<div>
<input type="radio" name="colors" (click)="color='lightgreen'">Green
<input type="radio" name="colors" (click)="color='yellow'">Yellow
<input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p [appHighlight]="color">Highlight me!</p>
修改 AppComponent.color
,讓它不再有初始值。
Path:"src/app/app.component.ts (class)"
export class AppComponent {
color: string;
}
下面是測(cè)試程序和指令的動(dòng)圖。
本例的指令只有一個(gè)可定制屬性,真實(shí)的應(yīng)用通常需要更多。
目前,默認(rèn)顏色(它在用戶選取了高亮顏色之前一直有效)被硬編碼為紅色。應(yīng)該允許模板的開發(fā)者設(shè)置默認(rèn)顏色。
把第二個(gè)名叫 defaultColor
的輸入屬性添加到 HighlightDirective
中:
Path:"src/app/highlight.directive.ts (defaultColor)"
@Input() defaultColor: string;
修改該指令的 onMouseEnter
,讓它首先嘗試使用 highlightColor
進(jìn)行高亮,然后用 defaultColor
,如果它們都沒有指定,那就用紅色作為后備。
Path:"src/app/highlight.directive.ts (mouse-enter)"
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || this.defaultColor || 'red');
}
當(dāng)已經(jīng)綁定過 appHighlight
屬性時(shí),要如何綁定到第二個(gè)屬性呢?
像組件一樣,你也可以綁定到指令的很多屬性,只要把它們依次寫在模板中就行了。 開發(fā)者可以綁定到 AppComponent.color
,并且用紫羅蘭色作為默認(rèn)顏色,代碼如下:
Path:"src/app/highlight.directive.ts (defaultColor)"
<p [appHighlight]="color" defaultColor="violet">
Highlight me too!
</p>
Angular 之所以知道 defaultColor 綁定屬于 HighlightDirective,是因?yàn)槟阋呀?jīng)通過 @Input 裝飾器把它設(shè)置成了公共屬性。
當(dāng)這些代碼完成時(shí),測(cè)試程序工作時(shí)的動(dòng)圖如下:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
color: string;
}
<h1>My First Attribute Directive</h1>
<h4>Pick a highlight color</h4>
<div>
<input type="radio" name="colors" (click)="color='lightgreen'">Green
<input type="radio" name="colors" (click)="color='yellow'">Yellow
<input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p [appHighlight]="color">Highlight me!</p>
<p [appHighlight]="color" defaultColor="violet">
Highlight me too!
</p>
/* tslint:disable:member-ordering */
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) { }
@Input() defaultColor: string;
@Input('appHighlight') highlightColor: string;
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || this.defaultColor || 'red');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
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 { }
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Attribute Directives</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root></app-root>
</body>
</html>
問題:為什么要加@Input
?
在這個(gè)例子中 hightlightColor
是 HighlightDirective
的一個(gè)輸入型屬性。你見過它沒有用別名時(shí)的代碼:
Path:"src/app/highlight.directive.ts (color)"
@Input() highlightColor: string;
也見過用別名時(shí)的代碼:
Path:"src/app/highlight.directive.ts (color)"
@Input('appHighlight') highlightColor: string;
無論哪種方式,@Input
裝飾器都告訴 Angular,該屬性是公共的,并且能被父組件綁定。 如果沒有 @Input
,Angular 就會(huì)拒絕綁定到該屬性。
但你以前也曾經(jīng)把模板 HTML 綁定到組件的屬性,而且從來沒有用過 @Input
。 差異何在?
差異在于信任度不同。 Angular 把組件的模板看做從屬于該組件的。 組件和它的模板默認(rèn)會(huì)相互信任。 這也就是意味著,組件自己的模板可以綁定到組件的任意屬性,無論是否使用了 @Input
裝飾器。
但組件或指令不應(yīng)該盲目的信任其它組件或指令。 因此組件或指令的屬性默認(rèn)是不能被綁定的。 從 Angular 綁定機(jī)制的角度來看,它們是私有的,而當(dāng)添加了 @Input
時(shí),Angular 綁定機(jī)制才會(huì)把它們當(dāng)成公共的。 只有這樣,它們才能被其它組件或?qū)傩越壎ā?/p>
你可以根據(jù)屬性名在綁定中出現(xiàn)的位置來判定是否要加 @Input
。
當(dāng)它出現(xiàn)在等號(hào)右側(cè)的模板表達(dá)式中時(shí),它屬于模板所在的組件,不需要 @Input
裝飾器。
當(dāng)它出現(xiàn)在等號(hào)左邊的方括號(hào)([ ]
)中時(shí),該屬性屬于其它組件或指令,它必須帶有 @Input
裝飾器。
試用此原理分析下列示例:
Path:"src/app/app.component.html (color)"
<p [appHighlight]="color">Highlight me!</p>
color
屬性位于右側(cè)的綁定表達(dá)式中,它屬于模板所在的組件。 該模板和組件相互信任。因此 color
不需要 @Input
裝飾器。appHighlight
屬性位于左側(cè),它引用了 HighlightDirective
中一個(gè)帶別名的屬性,它不是模板所屬組件的一部分,因此存在信任問題。 所以,該屬性必須帶 @Input
裝飾器。
更多建議: