iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
2
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
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
SuperMike
iT邦新手 5 級 ‧ 2019-05-24 16:32:41

DatePipe 的功能讓我想到 moment.js,請問能否在 Angular CLI 中引入 moment.js 呢?
是否用 npm 安裝以後,在某處引入呢?

Leo iT邦新手 3 級 ‧ 2019-05-28 13:11:24 檢舉

Hi SuperMike,

沒錯,就是這樣噢!

0
lila1002
iT邦新手 5 級 ‧ 2019-10-03 16:21:28

https://ithelp.ithome.com.tw/upload/images/20191003/20121830tHQW510umP.jpg

Hi!請問大大, 本人照著以上範例實作卻發生一個bug, 請問該怎麼解決呢

Leo iT邦新手 3 級 ‧ 2019-10-03 16:26:28 檢舉

Hi lila1002,

請參考官網範例的原始碼,我想你是沒有將 UnlessDirective 引入到 AppModule 裡。

如:

import { NgModule }      from '@angular/core';
import { FormsModule }   from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }         from './app.component';
import { heroSwitchComponents } from './hero-switch.components';

// 加入此行
import { UnlessDirective }    from './unless.directive';

@NgModule({
  imports: [ BrowserModule, FormsModule ],
  declarations: [
    AppComponent,
    heroSwitchComponents,
    
    // 加入此行
    UnlessDirective
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
lila1002 iT邦新手 5 級 ‧ 2019-10-03 16:58:17 檢舉

Hi!大大,你好 謝謝你的回覆
不好意思, 我那個範例應該是HighlightDirective這個,而app.module也已經import進去了, 還是不曉得為什麼錯誤; 然後hightlight.directive.ts內第10行一定要加入// tslint:disable-next-line: no-input-rename, 不然11行就會噴錯, 請大大點解為什麼, 好苦惱呀!
是否是我少了這支: src/app/unless.directive.ts
https://ithelp.ithome.com.tw/upload/images/20191003/20121830Q6IGnFYQz0.png

Leo iT邦新手 3 級 ‧ 2019-10-03 20:47:09 檢舉

Hi lila1002,

TSLint 那個就是設定而已,插入 // tslint:disable-next-line: no-input-rename 是針對當下的情況作單次規則調整,也可以直接到 tslint.json 直接改掉整份專案的規則。

至於其他的部份,由於你給的資訊不足,我不曉得你 HighlightDirective 裡寫了什麼,也不知道你的錯誤訊息是什麼,所以無從得知原因噢!@@

0
smile98
iT邦新手 5 級 ‧ 2020-06-02 17:26:50

Leo 大大你好:
我在練習“結構型的 Directive ”時出現這個問題
Identifier 'condition' is not defined. The component declaration, template variable declarations, and element references do not contain such a member
我有將 UnlessDirective 引入到 AppModule 裡
顯示出來是這樣,想請問我有哪個地方出錯嗎?
謝謝你:)

https://ithelp.ithome.com.tw/upload/images/20200602/20120589izGjm4iKyI.png
app.component.html
https://ithelp.ithome.com.tw/upload/images/20200602/20120589AdN8d3MHqo.png
unless.directive.ts (我是直接新增一個檔案沒有用ng generate directive unless )
https://ithelp.ithome.com.tw/upload/images/20200602/201205897vwRF5SyU6.png

看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2020-06-02 17:29:21 檢舉

Hi smile98,

看那個錯誤訊息的說明,貌似是你的 app.component.html 沒有 condition 這個變數,你再檢查看看! :)

smile98 iT邦新手 5 級 ‧ 2020-06-02 17:45:59 檢舉

Hi,
請問這個不算是變數嗎? :)
*appUnless="condition"
我最後是在app.component.ts
加了這行:)錯誤訊息就解決了
condition: boolean;

Leo iT邦新手 3 級 ‧ 2020-06-05 11:49:38 檢舉

Hi smile98,

噢,我打錯字了XD

我原本就是在說你 app.component.ts 沒有宣告 condition 這個變數,很高興你自己找到解決方法。 :)

smile98 iT邦新手 5 級 ‧ 2020-06-08 20:44:27 檢舉

謝謝你:)

0
thuartlynn
iT邦新手 5 級 ‧ 2021-11-09 17:36:56

請問是不是把HighlightDirective這個獨立成單支的XXX.ts
https://ithelp.ithome.com.tw/upload/images/20211109/20123332ZtHqQU3Dg3.png
然後再在app.component.ts引入XXX.ts,這裡引入後還需要再export AppComponent中加入嗎?)

而html的部分直接寫在app.component.html?

看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2021-11-09 17:47:11 檢舉

Hi thuartlynn,

請問是不是把HighlightDirective這個獨立成單支的XXX.ts

是,我自己甚至會讓它是獨立一個 Module ,會比較好被引用。

然後再在app.component.ts引入XXX.ts,這裡引入後還需要再export AppComponent中加入嗎?)

是在 xxx.module.ts 裡引用唷,只要有在 Module 裡引用,該 Module 的 Component 就會認得它

而html的部分直接寫在app.component.html?

謝謝作者大大~Leo

我看著你們前面的討論,在猜想是不是這樣的邏輯,

而且在condition這個變數要在 app.component.ts 中設定boolean這個寫法,我的VS Code自己修正為
condition!: boolean;
請問這樣對嗎?

再次感謝作者~Leo

不用幫我找了喔,我後來去點你給的官網範例,研究出來了!
再次謝謝~

Leo iT邦新手 3 級 ‧ 2021-11-17 16:58:08 檢舉

/images/emoticon/emoticon12.gif

我要留言

立即登入留言