屬性型指令用于改變一個 DOM 元素的外觀或行為。
在 Angular 中有三種類型的指令:
組件是這三種指令中最常用的。 你在快速上手例子中第一次見到組件。
結(jié)構(gòu)型指令修改視圖的結(jié)構(gòu)。例如,NgFor
和 NgIf
。 要了解更多,參見結(jié)構(gòu)型指令 指南。
屬性型指令改變一個元素的外觀或行為。例如,內(nèi)置的 NgStyle
指令可以同時修改元素的多個樣式。
屬性型指令至少需要一個帶有 @Directive
裝飾器的控制器類。該裝飾器指定了一個用于標(biāo)識屬性的選擇器。 控制器類實現(xiàn)了指令需要的指令行為。
本章展示了如何創(chuàng)建一個簡單的屬性型指令 appHighlight
,當(dāng)用戶把鼠標(biāo)懸停在一個元素上時,改變它的背景色。你可以這樣用它:
Path:"src/app/app.component.html (applied)"
<p appHighlight>Highlight me!</p>
注:
- 指令不支持命名空間。
在命令行窗口下用 CLI
命令 ng generate directive
創(chuàng)建指令類文件。
ng generate directive highlight
CLI
會創(chuàng)建 "src/app/highlight.directive.ts" 及相應(yīng)的測試文件("src/app/highlight.directive.spec.ts"),并且在根模塊 AppModule
中聲明這個指令類。
注:
- 和組件一樣,這些指令也必須在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
符號提供了 Angular 的 @Directive
裝飾器。
@Directive
裝飾器的配置屬性中指定了該指令的 CSS 屬性型選擇器 [appHighlight]
這里的方括號([]
)表示它的屬性型選擇器。 Angular 會在模板中定位每個擁有名叫 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
語句還從 Angular 的 core
庫中導(dǎo)入了一個 ElementRef
符號。
你可以在指令的構(gòu)造函數(shù)中使用 ElementRef
來注入宿主 DOM 元素的引用,也就是你放置 appHighlight
的那個元素。
ElementRef
通過其 nativeElement
屬性給你了直接訪問宿主 DOM 元素的能力。
這里的第一個實現(xiàn)把宿主元素的背景色設(shè)置為了黃色。
要想使用這個新的 HighlightDirective
,就往根組件 AppComponent
的模板中添加一個 <p>
元素,并把該指令作為一個屬性使用。
Path:"src/app/app.component.html"
<p appHighlight>Highlight me!</p>
運(yùn)行這個應(yīng)用以查看 HighlightDirective
的實際效果。
ng serve
總結(jié):Angular 在宿主元素 <p>
上發(fā)現(xiàn)了一個 appHighlight
屬性。 然后它創(chuàng)建了一個 HighlightDirective
類的實例,并把所在元素的引用注入到了指令的構(gòu)造函數(shù)中。 在構(gòu)造函數(shù)中,該指令把 <p>
元素的背景設(shè)置為了黃色。
當(dāng)前,appHighlight
只是簡單的設(shè)置元素的顏色。 這個指令應(yīng)該在用戶鼠標(biāo)懸浮一個元素時,設(shè)置它的顏色。
先把 HostListener
加進(jìn)導(dǎo)入列表中。
Path:"src/app/highlight.directive.ts (imports)"
import { Directive, ElementRef, HostListener } from '@angular/core';
然后使用 HostListener
裝飾器添加兩個事件處理器,它們會在鼠標(biāo)進(jìn)入或離開時進(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
裝飾器讓你訂閱某個屬性型指令所在的宿主 DOM 元素的事件,在這個例子中就是 <p>
。
當(dāng)然,你可以通過標(biāo)準(zhǔn)的 JavaScript 方式手動給宿主 DOM 元素附加一個事件監(jiān)聽器。 但這種方法至少有三個問題:
- 必須正確的書寫事件監(jiān)聽器。
- 當(dāng)指令被銷毀的時候,必須拆卸事件監(jiān)聽器,否則會導(dǎo)致內(nèi)存泄露。
- 必須直接和 DOM API 打交道,應(yīng)該避免這樣做。
這些處理器委托了一個輔助方法來為 DOM 元素(el
)設(shè)置顏色。
這個輔助方法(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 上的時候,背景色就出現(xiàn)了,而移開時就消失了。
高亮的顏色目前是硬編碼在指令中的,這不夠靈活。 在這一節(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
能用于綁定。
它之所以稱為輸入屬性,是因為數(shù)據(jù)流是從綁定表達(dá)式流向指令內(nèi)部的。 如果沒有這個元數(shù)據(jù),Angular 就會拒絕綁定,參見稍后了解更多。
試試把下列指令綁定變量添加到 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>
很不錯,但如果可以在應(yīng)用該指令時在同一個屬性中設(shè)置顏色就更好了,就像這樣:
Path:"src/app/app.component.html (color)"
<p [appHighlight]="color">Highlight me!</p>
[appHighlight]
屬性同時做了兩件事:把這個高亮指令應(yīng)用到了 <p>
元素上,并且通過屬性綁定設(shè)置了該指令的高亮顏色。 你復(fù)用了該指令的屬性選擇器 [appHighlight]
來同時完成它們。 這是清爽、簡約的語法。
你還要把該指令的 highlightColor
改名為 appHighlight
,因為它是顏色屬性目前的綁定名。
Path:"src/app/highlight.directive.ts (renamed to match directive selector)"
@Input() appHighlight: string;
這可不好。因為 appHighlight
是一個糟糕的屬性名,而且不能反映該屬性的意圖。
幸運(yùn)的是,你可以隨意命名該指令的屬性,并且給它指定一個用于綁定的別名。
恢復(fù)原始屬性名,并在 @Input
的參數(shù)中把該選擇器指定為別名。
Path:"src/app/highlight.directive.ts (color property with alias)"
@Input('appHighlight') highlightColor: string;
在指令內(nèi)部,該屬性叫 highlightColor
,在外部,你綁定到它地方,它叫 appHighlight
。
這是最好的結(jié)果:理想的內(nèi)部屬性名,理想的綁定語法:
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
改成一個測試程序,它讓你可以通過單選按鈕來選取高亮顏色,并且把你選取的顏色綁定到指令中。
把 "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;
}
下面是測試程序和指令的動圖。
本例的指令只有一個可定制屬性,真實的應(yīng)用通常需要更多。
目前,默認(rèn)顏色(它在用戶選取了高亮顏色之前一直有效)被硬編碼為紅色。應(yīng)該允許模板的開發(fā)者設(shè)置默認(rèn)顏色。
把第二個名叫 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
屬性時,要如何綁定到第二個屬性呢?
像組件一樣,你也可以綁定到指令的很多屬性,只要把它們依次寫在模板中就行了。 開發(fā)者可以綁定到 AppComponent.color
,并且用紫羅蘭色作為默認(rèn)顏色,代碼如下:
Path:"src/app/highlight.directive.ts (defaultColor)"
<p [appHighlight]="color" defaultColor="violet">
Highlight me too!
</p>
Angular 之所以知道 defaultColor 綁定屬于 HighlightDirective,是因為你已經(jīng)通過 @Input 裝飾器把它設(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
?
在這個例子中 hightlightColor
是 HighlightDirective
的一個輸入型屬性。你見過它沒有用別名時的代碼:
Path:"src/app/highlight.directive.ts (color)"
@Input() highlightColor: string;
也見過用別名時的代碼:
Path:"src/app/highlight.directive.ts (color)"
@Input('appHighlight') highlightColor: string;
無論哪種方式,@Input
裝飾器都告訴 Angular,該屬性是公共的,并且能被父組件綁定。 如果沒有 @Input
,Angular 就會拒絕綁定到該屬性。
但你以前也曾經(jīng)把模板 HTML 綁定到組件的屬性,而且從來沒有用過 @Input
。 差異何在?
差異在于信任度不同。 Angular 把組件的模板看做從屬于該組件的。 組件和它的模板默認(rèn)會相互信任。 這也就是意味著,組件自己的模板可以綁定到組件的任意屬性,無論是否使用了 @Input
裝飾器。
但組件或指令不應(yīng)該盲目的信任其它組件或指令。 因此組件或指令的屬性默認(rèn)是不能被綁定的。 從 Angular 綁定機(jī)制的角度來看,它們是私有的,而當(dāng)添加了 @Input
時,Angular 綁定機(jī)制才會把它們當(dāng)成公共的。 只有這樣,它們才能被其它組件或?qū)傩越壎ā?/p>
你可以根據(jù)屬性名在綁定中出現(xiàn)的位置來判定是否要加 @Input
。
當(dāng)它出現(xiàn)在等號右側(cè)的模板表達(dá)式中時,它屬于模板所在的組件,不需要 @Input
裝飾器。
當(dāng)它出現(xiàn)在等號左邊的方括號([ ]
)中時,該屬性屬于其它組件或指令,它必須帶有 @Input
裝飾器。
試用此原理分析下列示例:
Path:"src/app/app.component.html (color)"
<p [appHighlight]="color">Highlight me!</p>
color
屬性位于右側(cè)的綁定表達(dá)式中,它屬于模板所在的組件。 該模板和組件相互信任。因此 color
不需要 @Input
裝飾器。appHighlight
屬性位于左側(cè),它引用了 HighlightDirective
中一個帶別名的屬性,它不是模板所屬組件的一部分,因此存在信任問題。 所以,該屬性必須帶 @Input
裝飾器。
更多建議: