Angular學習筆記5 - 基本結構操作、Pipe、Directive

這次要繼續介紹與 Complnent 相關的各種工具:

  • Pipe
  • Directive

透過使用這些東西,可以使得 Component 樣板的內容變得更簡潔、好維護

在開始講之前,要先補之前沒有介紹到的一些基本的樣板結構操作

基礎的 Angular 樣板結構操作

Angular 提供了幾種結構性操作,使得撰寫樣板變得更加便利

結構性操作包括:

  • ngIf
  • ngFor
  • ngSwitch

ngIf

使用方式:<tag *ngIf="expression"> ... </tag>

用來決定是否顯示該元素以及其子元素的內容
裡面可以帶入變數,運算式,或常數

首先先準備好決定顯示的變數 visible

app.component.ts 部份
export class AppComponent {
visible = false;
}

接著來寫樣板

app.component.html
<p *ngIf="!visible"> contentA </p>
<p *ngIf="visible"> contentB </p>
<button (click)="visible=!visible"> toggle content </button>

樣板的內容是這樣的,一開始只會顯示 contentA
當點擊了 toggle content 後,會將 visible 的值做反向,此時顯示 contentB
再點擊一次 toggle content 後,再將 visible 反向一次,此時顯示 contentA

來看一下執行畫面:

0-0-1613996588336.gif

ngIf 為 false 時,是直接移除該元素

一般的 Javascript 做隱藏,可能會是設定 display:none 來達成隱藏的效果
而 ngIf 則會直接移除整個元素,顯示時再將其加回來

ngFor

使用方式:<tag *ngFor="let item of list;let i=index;let odd=odd; traceBy: func"> ... </tag>

可以遍歷 list 的內容,並將該元素以其子元素逐一輸出
裡面有幾個內容需要先說明清楚:

  • let item of list
    item 表示被迭代的元素,list 則是陣列
  • let i=index (可選)
    取得目前迴圈的索引值
  • let odd=odd (可選)
    目前的迭代的索引是否為奇數
  • trackBy: func (可選)
    指定用來追蹤物件的依據。通常不太會使用這個選項
    簡單來說,就是用來確定清單更新時的變化依據,來決定是否要重新載入 DOM 元素
    預設情況,當陣列內的物件不一樣時,會把整個元素都重新載入一次
    但可以指定像是名子之類的作為依據,這樣就只會更新有差異的部份,而不是整個 DOM 刪除重新加入
    算是可以增加前端顯示效能的功能

使用 ngFor 之前先設定一個字串陣列 list,等等要拿來遍歷用的

app.component.ts 部份
export class AppComponent {
list = [
'apple',
'banana',
'cat',
'dog',
]
}

然後是樣版

<style>
table, th, td {
border: 1px solid black;
}
</style>
<table>
<tr
*ngFor="let item of list;let i=index;let odd=odd;"
[style.background-color]="odd ? 'pink' : 'transparent'"
>
<td>{{ i }}</td>
<td>{{ item }}</td>
</tr>
</table>

樣板的部份,主要是將陣列的所有字串都印出來,並印出他們的索引值
為了使用 odd,這邊針對索引值為奇數的列,將背景顏色改為粉紅色

執行畫面:

image-20210222203142323

ngSwitch

使用方式:

<tag [ngSwitch]="variable"> 
<tag *ngSwitchCase="expression">
</tag>
<tag *ngSwitchCase="expression">
</tag>
<tag *ngSwitchDefault> <!-- 可選 -->
</tag>
</tag>

跟一般程式的 switch 差不多

這邊直接舉例子比較快:

app.component.ts 部份
export class AppComponent {
currentTab = '';
}
app.component.html
<button (click)="currentTab='Tab1'">Tab1</button>
<button (click)="currentTab='Tab2'">Tab2</button>
<button (click)="currentTab='Tab3'">Tab3</button>
<div [ngSwitch]="currentTab">
<p *ngSwitchCase="'Tab1'"> Tab1 </p>
<p *ngSwitchCase="'Tab2'"> Tab2 </p>
<p *ngSwitchCase="'Tab3'"> Tab3 </p>
<p *ngSwitchDefault> 404 </p>
</div>

這邊會模擬一個簡單的 Tab 功能

要特別注意的地方是在 *ngSwitchCase
*ngSwitchCase 的值是要輸入表達式,而非一般文字
所以當要使用字串作為判斷的依據時,兩旁邊要加上引號

執行畫面:

0-1613998385371.gif

Pipe

使用方式:value | pipe:arg1:arg2...

Pipe 是一種用來處理資料的工具
比較常見的例子像是格式化時間顯示

Pipe 的參數以冒號 : 做區隔

Pipe 可以透過 | 進行串接

官方提供的基本 Pipe

官方其實已經有提供幾個基本的 Pipe 來使用,像是:

詳細用法可以點擊上面的 Pipe 類別名稱前往 Angular 官網查看

Pipe 範例:

app.component.html
<p>{{ '2020-01-01' | date }}</p>
<p>{{ 'lower' | uppercase }}</p>
<p>{{ 'UPPER' | lowercase }}</p>
<p>{{ 100 | currency }}</p>
<p>{{ 10000 | number }}</p>
<p>{{ 0.05 | percent }}</p>

執行畫面:

image-20210222214250931

自訂 Pipe

Angular 使用者可以自己建立自訂的 Pipe 類別

最簡單的方式就是透過 Angular CLI 的 ng g pipe {pipe-path} 來建立

這邊會透過 Angular CLI 建立一個 Pipe html-tag

ng g pipe html-tag

建立好的 HtmlTagPipe 如下:

html-tag.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'htmlTag'
})
export class HtmlTagPipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}

透過 @Pipe 裝飾器來建立可使用的 Pipe
name 表示在樣板使用時的 Pipe 名稱

由於之前建立專案時,是使用型別嚴格檢查,所以產生的函式參數型別會是 unknown 的型態

接下來會假定傳入數值一定是字串,然後將輸出字串 <value>

修改後的 transform 函式如下:

html-tag.pipe.ts 部份
transform(value: string): string {
return `<${value}>`;
}

接著使用建立好的 Pipe

app.component.html
<p>{{ 'li' | htmlTag }}</p>

執行畫面:

image-20210222214454420

Directive

Directive 可以修飾元素本身

Directive 修飾的元素都是透過選擇器來選擇
通常是選擇含有自訂屬性 (attribute) 的元素
(在 CSS Selector 中表示為 [attribute] )

Angular 有提供一些基本的 Directive:

  • ngClass
    CSS 樣式類別
  • ngStyle
    CSS 樣式
  • Model
    對 HTML Form 元素進行雙向綁定時使用

ngIf、ngFor 其實都是 Directive

前面在說明結構性操作時,有提到 ngIf、ngFor 跟 ngSwitch
這些其實全部都是 Directive
他們在官方文件中被歸類為結構指示 (Structural Directive)

Directive 的特例:Component

Component 本身繼承自 Directive
可以說是 Directive 的特例
專門修飾自訂標籤,並輸出樣板內容
Directive 可以做的事情,Component 都可以做

自訂 Directive

簡單的介紹了 Directive 之後,接著來新增自定義的 Directive

本次將會建立一個名為 icon 的 Directive

在開始之前,先透過 npm 安裝 icon 套件 FontAwesome

npm install @fortawesome/fontawesome-free

接著在 專案資料夾/angular.json 中引入 FontAwesome 的 CSS 檔與 JS 檔

angular.json 部份
"styles": [
/* ... */
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
/* ... */
],
"scripts": [
/* ... */
"node_modules/@fortawesome/fontawesome-free/js/all.min.js"
/* ... */
]

接著使用 Angular CLI 建立一個 Directive,指令如下:

ng g directive icon

建立好的 Directive 會長這個樣子:

icon.directive.ts
import { Directive } from '@angular/core';

@Directive({
selector: '[appIcon]'
})
export class IconDirective {
constructor() { }
}

接著來改寫一下,在建構式引入元素參考
並增加 class fafa-home

icon.directive.ts 部份
export class IconDirective {
constructor(elem: ElementRef<HTMLElement>) {
elem.nativeElement.classList.add('fa', 'fa-home');
}
}

然後在 AppComponent 的樣板裡套用這個 Directive

app.component.html
<button>
<i appIcon></i> <span> Home </span>
</button>

執行結果:

image-20210223100039003

Input

只有一種 Icon 顯然是不夠的
為了能夠讓使用者自己決定要使用的 Icon,因此要在 Directive 裡新增輸入變數

icon.directive.ts
import { Directive, ElementRef, Input } from '@angular/core';

@Directive({
selector: '[appIcon]'
})
export class IconDirective {
@Input() iconType = 'home';

constructor(elem: ElementRef<HTMLElement>) {
elem.nativeElement.classList.add('fa', `fa-${this.iconType}`);
}
}

建構式的內容也改成會隨著 iconType 而改變

使用了 Input 之後,就可以在樣板裡再新增一個 iconType 屬性來決定輸出數值

app.component.html
<button>
<i appIcon iconType="link"></i> <span> Url </span>
</button>

執行結果:

image-20210223102046493

嗯…?怎麼沒改變?
原來 Directive 在建構時就已經設定好元素的樣式了
因此即使後來設定 iconType,也沒辦法改變樣式
為了解決這個問題,需要讓 IconDirective 繼承 OnInit 界面

OnInit 介面

OnInit 界面裡只有一個函式 ngOnInit
當一切都設定好,頁面要初始化時,就會呼叫 ngOnInit
因此我們要來改寫原本的 Directive,使其繼承 OnInit 並實做 ngOnInit

icon.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
selector: '[appIcon]'
})
export class IconDirective implements OnInit {
@Input() iconType = 'home';

// 這邊使用了 TypeScript 的語法糖
// 引入變建立一個私有成員 elem
constructor(private elem: ElementRef<HTMLElement>) {
}

// 在初始化時才設定 icon
ngOnInit() {
this.elem.nativeElement.classList.add('fa', `fa-${this.iconType}`);
}
}

接著看一下執行畫面:

image-20210223102003663

前面寫完 IconDirective 之後,感覺比以往直接賦予 class 還要麻煩,字也更多
每次設定 Icon 時都要出現 appIconiconType
要解決這個問題,可以使用 Input 屬性綁定

Input 屬性綁定

從前面的範例中可以發現,Input 本身也是一個裝飾器
而 Input 本身其實可以接受一個參數,也就是要綁定的屬性名稱

在這邊為了要簡化 iconTypeappIcon 一定要一起出現的問題
因此對於 iconType 的部份就透過綁定 appIcon 來解決:

icon.directive.ts 部份
@Input('appIcon') iconType = 'home';

樣板的內容就可以進行改寫:

app.component.html
<button>
<i appIcon="link"></i> <span> Url </span>
</button>

執行結果:

image-20210223103332732

事實上將 Property 名稱設定為 appIcon,也可以達成一樣的效果
不一定要進行屬性綁定

結語

這次算是補足了之前沒說到的 ngIf、ngFor 等結構指示
雖然是知道 ngIf 跟 ngFor 或是 ngSwitch 這些都是 Directive
但由於其特殊性,讓我不知道該怎麼說明才好
後來看到官方文件上寫的是 Structural Directive
於是前面在不破題的情況下,我就把這些都稱為結構性操作

ngModel 的部份由於涉及雙向綁定
自己本身對這部份不熟,所以沒另外講解
可以參考官方的雙向綁定條目來進一步了解

這次篇幅跟之前比起來有比較長一點
實際上 Directive 還有其他東西可以說明
但是由於篇幅關係,加上有些內容我也不是很熟悉,因此就不另外介紹了
有興趣的可以去找官方文件來深入

參考