Angular9 屬性型指令

2020-06-30 18:03 更新

屬性型指令用于改變一個(gè) DOM 元素的外觀或行為。

指令概覽

在 Angular 中有三種類型的指令:

  1. 組件 — 擁有模板的指令

  1. 結(jié)構(gòu)型指令 — 通過添加和移除 DOM 元素改變 DOM 布局的指令

  1. 屬性型指令 — 改變?cè)?、組件或其它指令的外觀和行為的指令。

組件是這三種指令中最常用的。 你在快速上手例子中第一次見到組件。

結(jié)構(gòu)型指令修改視圖的結(jié)構(gòu)。例如,NgForNgIf。 要了解更多,參見結(jié)構(gòu)型指令 指南。

屬性型指令改變一個(gè)元素的外觀或行為。例如,內(nèi)置的 NgStyle 指令可以同時(shí)修改元素的多個(gè)樣式。

創(chuàng)建一個(gè)簡(jiǎn)單的屬性型指令

屬性型指令至少需要一個(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è)置為了黃色。

響應(yīng)用戶引發(fā)的事件

當(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í)就消失了。

使用 @Input 數(shù)據(jù)綁定向指令傳遞值

高亮的顏色目前是硬編碼在指令中的,這不夠靈活。 在這一節(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 屬性

注意看 @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è)糟糕的屬性名,而且不能反映該屬性的意圖。

綁定到 @Input 別名

幸運(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;
  }
}

測(cè)試程序

憑空想象該指令如何工作可不容易。你需要把 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è)屬性

本例的指令只有一個(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)圖如下:

源代碼

  1. Path:"src/app/app.component.ts"

    import { Component } from '@angular/core';


    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      color: string;
    }

  1. Path:"src/app/app.component.html"

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

  1. Path:"src/app/highlight.directive.ts"

    /* 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;
      }
    }

  1. Path:"src/app/app.module.ts"

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

  1. Path:"src/app/main.ts"

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

  1. Path:"src/app/index.html"

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

小結(jié)

  • 構(gòu)建一個(gè)屬性型指令,它用于修改一個(gè)元素的行為。

  • 把一個(gè)指令應(yīng)用到模板中的某個(gè)元素上。

  • 響應(yīng)事件以改變指令的行為。

  • 把值綁定到指令中。

附錄

問題:為什么要加@Input

在這個(gè)例子中 hightlightColorHighlightDirective 的一個(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 裝飾器。
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)