iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
1
Modern Web

Angular 深入淺出三十天系列 第 9

[Angular 深入淺出三十天] Day 08 - 基礎結構說明(三)

「原來 Angular 的 Template 語法跟資料綁定有這麼多名堂阿?!」Wayne 揉揉他的太陽穴,貌似有點快消化不良的樣子。

「是阿!但這一切其實都是為了讓我們可以更方便地使用、更容易閱讀、維護與重用,可謂用心良苦阿!」我說完便遞給他兩顆口香糖,讓他提提神。

「那接下來呢?」Wayne 嚼了嚼口香糖後說道。

「接下來我們來聊聊之前用過的 pipe*ngFor 吧!其實...」


Pipe

很多時候當我們前端拿到資料或是我們要將資料送給後端時,往往都跟我們畫面上所需要顯示的東西不一樣。拿時間來說,有時候我們從後端拿到的東西會是 Timestamp 的格式像是 1540102625599 ,但其實我們畫面上必須顯示的必須像是 2018/10/21 14:17:05 。這時候該怎麼辦?

謎之音:自己寫阿不然咧?!

好,所以我們自己寫出了類似這樣子的東西 (我隨便寫寫不要鞭我)

var dateObj = new Date(1540102625599);
var years = dateObj.getFullYear();
var months = dateObj.getMonth() + 1;
var dates = dateObj.getDate();
var hours = dateObj.getHours();
var mins = dateObj.getMinutes();
var seconds = dateObj.getSeconds();

console.log(years + '/' + months + '/' + dates + ' ' + hours + ':' + mins + ':' + seconds);

寫過的舉手!!

如果使用 DatePipe 的話:

{{today | date:'yyyy/MM/dd HH:mm:ss'}}

一行結束!!輕鬆又愉快!!

當然,現在有很多套件其實也都可以很輕鬆地辦到,但其實 Angular 除了 DatePipe 之外,很貼心地提供了非常多類似的 Pipe 如:

如果上述這些都沒辦法滿足你的需求,沒關係!一樣自己做:

import { Pipe, PipeTransform } from '@angular/core';

/*
 * Raise the value exponentially
 * Takes an exponent argument that defaults to 1.
 * Usage:
 *   value | exponentialStrength:exponent
 * Example:
 *   {{ 2 | exponentialStrength:10 }}
 *   formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

上面是官網所提供的一個客製化計算次方數的 Pipe 的範例。範例中可以看到名為 @Pipe 的裝飾器,Metadata 裡的 name 指的是這個 Pipe 在使用時的名字 exponentialStrength,類別則要實作 PipeTransformtransform 函式,而傳入 transform 函式中的 value 是原本的值; exponent 則是要計算的次方數。

接著只要像這樣使用:

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <div>Normal power: <input [(ngModel)]="power"></div>
    <div>Boost factor: <input [(ngModel)]="factor"></div>
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `
})
export class PowerBoostCalculatorComponent {
  power = 5;
  factor = 1;
}

就可以完成像是這樣的功能:

Imgur


Directive

Directive 在 Angular 裡也是一個很重要的存在,嚴格說起來,Component 也是 Directive。只是 Component 是 Angular 比較獨特且核心的部分,所以它才用 @Component 的裝飾器,而不是用 @Directive

那至今我們用過的功能中,有什麼是 Directive 呢?答案是: *ngfor

Directive 其實分兩種:

  • 結構型-結構型的 Directive 透過新增、刪除或是取代 DOM 中的元素來更改 Layout。像之前用的 *ngFor 就是結構型的 Directive,它會根據資料的數量新增 DOM 中的元素。值得一提的是,通常結構型的 Directive 會加上 * 的前綴。

  • 屬性型-屬性型的 Directive 則是可以更改現有元素的外觀或行為。因其在 Template 中看起來就像是 HTML Tag 的屬性一樣,故以此命名。之前提到過的 [(ngModel)] 其實就是屬性型的 Directive。

那結構型的 Directive 怎麼寫呢?我們一樣來參考官網的範例:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

/**
 * Add the template content to the DOM unless the condition is true.
 */
@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;
    }
  }
}

@Directive 裝飾器裡的 Metadata 是在 Template 中使用時的名字,然後我們可以透過 constructor 函式的參數 templateRefviewContainer 取得使用該 Directive 的 Template 與視圖容器;再根據傳入 appUnlesscondition 來決定是要透過 viewContainertemplateRef 新增進畫面裡,還是要清空 viewContainer 裡的東西。

接著再到 Template 中使用:

<p *appUnless="condition" class="unless a">
  (A) This paragraph is displayed because the condition is false.
</p>

<p *appUnless="!condition" class="unless b">
  (B) Although the condition is true,
  this paragraph is displayed because appUnless is set to false.
</p>

就可以完成這樣子的效果:

Imgur

結構型的 Directive 寫完了,我們來試著寫寫看屬性型的吧!繼續參考官網的範例:

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}

裝飾器的部份相信大家都知道所以就不再贅述了。透過 constructor 的參數 el 可以拿到當前使用這個 Directivce 的元素實體。然後只要在 Template 中加入這個 Directive 就可以將其背景變成黃色:

<p appHighlight>Highlight me!</p>

但是這樣好無聊,我們希望它可以更有趣一點!例如將滑鼠游標移入時,背景才變色;滑鼠游標移出之後則變回原來的顏色。既然如此,就需要來偵測滑鼠游標移入與移出的事件。該怎麼做呢?

首先我們可以將一個叫做 HostListener 的裝飾器 import 進來:

import { Directive, ElementRef, HostListener } 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;
}

@HostListener 裝飾器讓我們的 onMouseEnter/onMouseLeave 函式在父層元素發生對應的事件時會被觸發,讓我們的 Directive 可以相應地切換背景顏色。

Imgur

但目前 Hightlight 的顏色都是固定黃色,這樣也不太合理,應該要能在使用時指定 Hightlight 的顏色才對!怎麼做呢?我們再從 @angular/core 多 import 一個叫做 Input 的裝飾器進來:

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

並且新增一個名為 highlightColor 的屬性,準備用來存取使用者傳進來的顏色字串:

@Input() highlightColor: string;

@Input() 裝飾器令我們的 highlightColor 可以用以下兩種方式接受父層傳進來的資料:

<p appHighlight highlightColor="yellow">Highlighted in yellow</p>
<p appHighlight [highlightColor]="'orange'">Highlighted in orange</p>

有注意到嗎?這兩個方式的差別在於屬性名稱有沒有用 [] 包住。如果該屬性的值是純字串,就不要用 [] 包住屬性名稱;如果該屬性的值是存放著值的某個變數,則記得要用 [] 包住參數名稱。

但目前 appHighlight 這個名稱只是純裝飾,每次使用時要加兩個屬性好不方便,我們來調整一下:

@Input('appHighlight') highlightColor: string;

appHighlight 這個名字當做參數傳入 @Input 裝飾器裡。意思是父層要傳資料進來時,就要使用 appHighlight 這個屬性名稱,但對於這個類別來說,這個變數的名稱其實還是 highlightColor

<p [appHighlight]="color">Highlight me!</p>

所以最終的程式碼大概會長這樣:

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

參考資料


錯誤更新記錄

  • 2018/10/25 21:20 - 非常感謝邦友 mis10302 的提醒,修正所有的範例圖片都用到同一張的問題。

上一篇
[Angular 深入淺出三十天] Day 07 - 基礎結構說明(二)
下一篇
[Angular 深入淺出三十天] Day 09 - Angular 小學堂(二)
系列文
Angular 深入淺出三十天33

尚未有邦友留言

立即登入留言