在 Angular 中 Directive 分成三種:

  1. Component:帶有 templete 的 directive。
  2. Structural Directive:可以改變 DOM 的結構,如:NgIf, NgFor, 以及 NgSwitch
  3. Directive:可以改變 DOM ,或 component 呈現的樣式或行為,如NgClass , NgStyle

今天,我要為各位介紹如何使用客製化 directive 來改變一段文字的背景顏色

  1. 首先 component 的 templete

    1
    <p>萊納,你坐啊!</p>
  2. 建立 directive 指令,跟 component 一樣,只是將 c 改成 d (directive 簡寫),取名 add-style

    1
    ng g d add-style
  3. 來看看新建立的 add-style.directive.ts

    1
    2
    3
    4
    5
    6
    @Directive({
    selector: '[addStyle]'
    })
    export class AddStyleDirective {
    constructor() { }
    }
    • @Directive:這是 Decorator ,定義 class 為 directive。
    • selector: 決定套用在目標( DOM 或 Component )上的屬性( Attribute )名稱,如果名稱是[addStyle],那 <p addStyle>...</p><div addStyle>...</div> 都會吃到,但若名稱是 p[addStyle] ,則只有 <p addStyle>...</p> 會吃到。
  4. 我們可以藉由注入 ElementRef,使用 ElementRef 的 nativeElement 屬性取得宿主的 DOM
    add-style.directive.ts

    1
    2
    3
    4
    5
    6
    export class AddStyleDirective {
    constructor(private elRef: ElementRef) {}
    ngOnInit(): void {
    console.log(this.elRef.nativeElement);
    }
    }

    template

    1
    <p addStyle>萊納,你坐啊!</p>

    console.log

    1
    <p _ngcontent-wpy-c12="" addstyle="">萊納,你坐啊!</p>
  5. 使用 DOM API 來改變樣式

    1
    2
    3
    4
    5
    6
    7
    export class AddStyleDirective {
    constructor(private elRef: ElementRef) {}

    ngOnInit(): void {
    this.elRef.nativeElement.style.backgroundColor = 'tomato';
    }
    }

    呈現如下

    上述的方式,可以讓我們很方便地取得 DOM 更改樣式,但 Angular 官方並不推薦這種方式,會產生以下問題:

    • 直接訪問 DOM,會有 XSS 攻擊的風險。
    • 會使應用( application )和渲染層( rendering )之間產生緊密耦合。這將導致無法分開兩者,也就無法將應用程式發佈到 Web Worker 中
      因此,Angular 官方推薦使用 Renderer2 ,避免以上問題。
  6. Renderer2 注入後,使用其 setStyle 函式設定樣式,優雅地解決了此問題

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export class AddStyleDirective {

    constructor(private elRef: ElementRef, private renderer: Renderer2) {}

    ngOnInit(): void {
    // this.elRef.nativeElement.style.backgroundColor = 'tomato';
    this.renderer.setStyle(
    this.elRef.nativeElement,
    'backgroundColor',
    'tomato'
    );
    }
    }
  7. 但如果將設定值寫死在程式內,會非常沒有彈性,所以我們會使用@Input(),將屬性值從外部傳入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    export class AddStyleDirective {

    @Input() backgroundColor: string;

    constructor(private elRef: ElementRef, private renderer: Renderer2) {}

    ngOnInit(): void {
    this.renderer.setStyle(
    this.elRef.nativeElement,
    'backgroundColor',
    this.backgroundColor
    );
    }
    }

    Template

    1
    <p addStyle [backgroundColor]="'tomato'">萊納,你坐啊!</p>
  8. 進一步優化 directive 程式碼,我們將樣式的邏輯從 ngOnInit( ) 移到 @Input( ) setter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export class AddStyleDirective {
    @Input() set backgroundColor(color: string) {
    this.renderer.setStyle(this.elRef.nativeElement, 'backgroundColor', color);
    }

    constructor(private elRef: ElementRef, private renderer: Renderer2) {}

    ngOnInit(): void {}
    }
  9. 同樣地,template 也是有優化的空間,p element 包括了 addStyle directive 與 backgroundColor @Input property,這段也是可以簡化的

    1
    <p addStyle [backgroundColor]="'tomato'">萊納,你坐啊!</p>
  10. 將 @Input 與 directive selector 設定相同的名稱(addStyle)

    1
    2
    3
    @Input() set addStyle(color: string) {
    this.renderer.setStyle(this.elRef.nativeElement, 'backgroundColor', color);
    }
  11. 我們可以技巧性地將第一個 addStyle 拿掉,只留下 [addStyle]

    1
    <p [addStyle]="'tomato'">萊納,你坐啊!</p>

    可以同時完成兩件事: - 不僅可以使用這個 directive - 同時,也可以使用 directive 上所設定的 @Input property

  12. 但這會有個問題,@Input property 名稱變成了 addStyle,變得不明確,我們可以使用別名來解決此問題

    1
    2
    3
    @Input('addStyle') set backgroundColor(color: string) {
    this.renderer.setStyle(this.elRef.nativeElement, 'backgroundColor', color);
    }

    如此一來,不僅可以維持原本的簡潔,更賦予@Input property 有意義的名稱,如:backgroundColor

以上,就是製作 attribute directive 的方式,這樣做的好處在於,可將相同的顯示樣式,套用在多個 element 上,至於實作的細節,即便再怎樣複雜,我們只要專注於 directive 本身就好。