[從 0 開始的 Angular 生活]No.21 Angular 結構型指令 (Structural Directives)

前言

總算到介紹到最後一個 Angular 的指令 - 結構型指令 (Structural Directives) 啦,打起精神繼續往下學習吧!

結構型指令 (Structural Directives)

這種結構型的指令會透過新增或刪除 DOM 的元素動態改變 DOM 的結構,內建有三種語法:

  • ngIf
  • ngSwitch
  • ngFor

相信看到這些語法大概也猜得出七八成是在做什麼的,以下一一示範如何使用。

ngIf

ngIf 的用法相當簡單,可以透過這個指令動態的新增或者是刪除一整段 HTML 的內容。

假設這個 ICON 區塊會根據 Counter 的數字動態的顯示或隱藏,那該怎麼做呢?

首先找到 Template 內對應的 HTML 區塊,並進行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="social-icons" class="pull-right social-icon" *ngIf="counter % 2 == 0">
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/facebook.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/twitter.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/googleplus.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/plurk.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/weibo.png">
</a>
<a href="https://www.google.com" title="RSS 訂閱" target="_blank">
<img src="/assets/images/rss.png">
</a>
</div>

特別的是 ngIf 前面必須加上一個 * 號,這個星號是結構型指令專屬的語法,算是語法糖的一種。

只要是結構型指令,記得加上 * 號就對了。,不過我們也不需要真的死背,可以善用 code snippet 來輔助。

*ngIf 的等號後方同樣也是接布林值,因此在這裡我們填入 counter % 2 == 0 這樣就可以了。

  • 只要回傳 True ,那麼就新增這個 HTML 區塊,反之則刪除

測試看看吧!

新增

刪除

這邊要強調的是 ngIf 是真的操作 DOM 元素動態改變結構進行新增或是刪除,也就是當回傳 False 時該區塊是完全不存在網頁中的,並非單純的隱藏。

需要注意的地方

使用 ngif 時,雖然語法相當的簡單,可是有生命週期的細節需要注意。

使用 ngif 新增或刪除某個區塊的 HTML 時,那些區塊可能含有其他的 Component

  • *ngif = false 時 Component 會一起被刪除
  • *ngif = True 時 Component 被新增回來

也就是說這樣的情況下會影響到 Component 的生命週期,當 Component 被刪除時生命週期也就跟著結束了。

因此下一次新增回來的 Component 狀態肯定會跟之前的不同,這點需要特別注意

這個 Component 狀態好比 HeaderComponent 內的 counter ,假如目前 counter = 5 當元件被刪除又新增後,這個時候的 counter 已經被重置了。

ngSwitch

ngSwitch 也是個結構型指令,假設我們有個需求是這樣:

  • 根據 ngSwitch 去切換前三個 ICON 與後三個 ICON 的內容
  • 切換的條件根據 counter % 2 產生的不同的餘數來切換

首先找到 Template 內對應的 HTML 區塊,並進行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div id="social-icons" class="pull-right social-icon">
<div [ngSwitch]="counter % 2">
<div *ngSwitchCase="0">
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/facebook.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/twitter.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/googleplus.png">
</a>
</div>
<div *ngSwitchCase="1">
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/plurk.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/weibo.png">
</a>
<a href="https://www.google.com" title="RSS 訂閱" target="_blank">
<img src="/assets/images/rss.png">
</a>
</div>
<div *ngSwitchDefault>N / A</div>
</div>
</div>

[ngSwitch] 後面接的是條件,在這裡我們的條件設定為 counter % 2 ,而 *ngSwitchCase 後面接的是「當遇到 0 或 1 時要顯示哪些東西」,那如果都不符合的話的則顯示 *ngSwitchDefault 的結果。

存檔測試看看吧!

成功是成功了,但這時會發現使用 ngSwitch 導致 HTML 結構不一致。

使用 ngSwitch 導致 HTML 結構不一致

這該怎麼修正呢?

可以使用 ng-container 來取代使用 ngSwitch 時產生的 div 標籤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div id="social-icons" class="pull-right social-icon">
<ng-container [ngSwitch]="counter % 2">
<ng-container *ngSwitchCase="0">
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/facebook.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/twitter.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/googleplus.png">
</a>
</ng-container>
<ng-container *ngSwitchCase="1">
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/plurk.png">
</a>
<a href="https://www.google.com" target="_blank">
<img src="/assets/images/weibo.png">
</a>
<a href="https://www.google.com" title="RSS 訂閱" target="_blank">
<img src="/assets/images/rss.png">
</a>
</ng-container>
<ng-container *ngSwitchDefault>N / A</ng-container>
</ng-container>
</div>

再次觀察 DOM 結構

此時就沒有多餘的 DIV 標籤了。

從這邊我們知道,當使用結構型指令時可以使用 ng-container 標籤,使其不會產生出額外的 HTML 標籤。

ngFor

ngFor 的功能相當強大,使用的時機也相當頻繁。

舉例來說,當我們串接 API 的時候,會取得一份資料。此時可能會透過迴圈的方式顯示在畫面上,這時後就有機會用上 ngFor 。

因為目前網頁的文章是寫死的,接下來要透過 ngFor 動態的把文章的資料呈現在網頁上。

處理資料部分

先整理好要給 ngFor 跑的陣列資料,可以先寫死一份假資料在 app.component.ts 內。

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
/* tslint:disable */
export class AppComponent {
inputValue = '';
atticleData = [
{
"id": 1,
"href": "http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx",
"title": "從命令提示字元中開啟 Visual Studio Code 如何避免顯示惱人的偵錯訊息",
"date": "2016/04/30 18:05",
"author": "GHJKL",
"category": "Visual Studio",
"category-link": "http://blog.miniasp.com/category/Visual-Studio.aspx",
"summary": "<p>由於我的 Visual Studio Code 大部分時候都是在命令提示字元下啟動,所以只要用 <strong><font color='#ff0000' face='Consolas'>code .</font></strong>就可以快速啟動 Visual Studio Code 並自動開啟目前所在資料夾。不過不知道從哪個版本開始,我在啟動 Visual Studio Code 之後,卻開始在原本所在的命令提示字元視窗中出現一堆惱人的偵錯訊息,本篇文章試圖解析這個現象,並提出解決辦法。</p><p>... <a class='more' href='http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx#continue'>繼續閱讀</a>...</p>"
},
{
"id": 2,
"href": "http://blog.miniasp.com/post/2016/03/22/Does-Certification-Exam-Useful.aspx",
"title": "考證照真的沒用嗎?一個從業 20 年的 IT 主管告訴你他怎麼看!",
"date": "2016/03/22 19:28",
"author": "GHJKL",
"category": "心得分享",
"category-link": "http://blog.miniasp.com/category/%E5%BF%83%E5%BE%97%E5%88%86%E4%BA%AB.aspx",
"summary": "<p>其實無論在哪個國家都有推行證照制度,且行之有年,台灣當然也不例外,這件事一開始的立意都是好的,就是希望透過一套公平的考試制度,評估一個人的技術能力是否達到一定程度水準,不但能當成一個人的能力指標,也可以讓大家有個明確目標朝專業之路邁進。其他的行業我不清楚,但就我本身熟悉的 IT 產業來說,不知何年何月開始,大家開始對證照制度嗤之以鼻、不屑一顧,甚至覺得是一個人能力的<strong>負指標</strong> (也就是能力不好的人才需要靠證照證明自己)。你說這現象是何等的詭異?是什麼樣的天時、地利、人和,可以讓一個原本立意良善的制度,變成人人喊打的落水狗,可能連有張證照都還不敢承認的地步。今天,就來談談我的個人見解。</p><p>... <a class='more' href='http://blog.miniasp.com/post/2016/03/22/Does-Certification-Exam-Useful.aspx#continue'>繼續閱讀</a>...</p>"
},
{
"id": 3,
"href": "http://blog.miniasp.com/post/2016/03/14/ASPNET-MVC-Developer-Note-Part-28-Understanding-ModelState.aspx",
"title": "ASP.NET MVC 開發心得分享 (28):深入瞭解 ModelState 內部細節",
"date": "2016/03/14 12:14",
"author": "GHJKL",
"category": "ASP.NET MVC",
"category-link": "http://blog.miniasp.com/category/ASPNET-MVC.aspx",
"summary": "<p>在 ASP.NET MVC 的 <strong>模型繫結</strong> (Model Binding) 完成之後,我們可以在 Controller / Action 中取得 <span style='font-family: Consolas;'>ModelState</span> 物件,一般來說我們都會用 <span style='font-family: Consolas;'><strong>ModelState.IsValid</strong></span> 來檢查在「模型繫結」的過程中所做的<strong>輸入驗證</strong> (Input Validation) 與 <strong>模型驗證</strong> (Model Validation) 是否成功。不過,這個 <span style='font-family: Consolas;'>ModelState</span>物件的用途很廣,裡面存有非常多模型繫結過程的狀態資訊,不但在 Controller 中能用,在 View 裡面也能使用,用的好的話,可以讓你的 Controller 更輕、View 也更乾淨,本篇文章將分享幾個 ModelState 的使用技巧。</p><p>... <a class='more' href='http://blog.miniasp.com/post/2016/03/14/ASPNET-MVC-Developer-Note-Part-28-Understanding-ModelState.aspx#continue'>繼續閱讀</a>...</p>"
}
]

cleanInput() {
this.inputValue = '';
}
}

在這邊因為 tslint 要求我把所有的雙引號改成單引號,為了開發方便,我先暫時把 tslint 給關閉。

就這樣我們新增了一個 atticleData 屬性,裡面是一個陣列,陣列內又有一個個物件,而每一個物件又有相關的屬性。

接著我們透過 ngFor 這個結構型指令來把這些通通顯示在畫面上吧!

處理 Template 的 HTML 部分

回過頭來處理 HTML 部分,同樣我們使用 code snippet 幫助撰寫程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Article START-->
<article class="post" id="post0" *ngFor="let item of atticleData">
<header class="post-header">
<h2 class="post-title">
<a [href]="item.href">{{ item.title }}</a>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
</header>
<section class="post-body text">
{{item.summary}}
</section>
</article>
<!-- Article END-->

這裡的 *ngFor 後面接的語法有點像是 JavaScript 中 ForEach() 的感覺, item 代表當前陣列的元素而 atticleData 則是要遍歷的目標陣列。

蠻像 Vue 中的 v-for 指令的,如果有學習過 Vue 肯定對這個部分不陌生。

運行開發伺服器,觀察結果吧!

至此,我們已經成功地透過 ngFor 快速的把資料轉換成網頁的內容,而且程式碼相當的簡潔。

不過還需要修正最後一個小地方,那就是文章 summary 部分的 HTML 標籤居然直接被輸出了,這該怎麼辦呢?

透過屬性繫結的方式

先把 HTML 中的 這段內嵌繫結刪除,並且套用屬性繫結

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Article START-->
<article class="post" id="post0" *ngFor="let item of atticleData">
<header class="post-header">
<h2 class="post-title">
<a [href]="item.href">{{ item.title }}</a>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
</header>
<section class="post-body text" [innerHTML]="item.summary">
</section>
</article>
<!-- Article END-->

這一段的作法是將屬性繫結在 section 標籤的 DOM 物件內的 innerHTML 屬性上,這麼一來 HTML 的標籤就會直接寫入 innerHTML 這個屬性內。

如此一來就全部搞定啦~

值得注意的是這個作法是有風險的,因為這些 HTML 可能含有一些惡意的內容,可能會導致 Cross-Site Script 的攻擊。

不過 Angular 有幫我們做到一些很基本的防護,例如在資料的部分偷偷的加一些料:

1
2
3
4
5
6
7
8
9
10
11
12
13
atticleData = [
{
"id": 1,
"href": "http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx",
"title": "從命令提示字元中開啟 Visual Studio Code 如何避免顯示惱人的偵錯訊息",
"date": "2016/04/30 18:05",
"author": "GHJKL",
"category": "Visual Studio",
"category-link": "http://blog.miniasp.com/category/Visual-Studio.aspx",
"summary": "<script>alert('XSS!!!')</script><p>由於我的 Visual Studio Code 大部分時候都是在命令提示字元下啟動,所以只要用 <strong><font color='#ff0000' face='Consolas'>code .</font></strong>就可以快速啟動 Visual Studio Code 並自動開啟目前所在資料夾。不過不知道從哪個版本開始,我在啟動 Visual Studio Code 之後,卻開始在原本所在的命令提示字元視窗中出現一堆惱人的偵錯訊息,本篇文章試圖解析這個現象,並提出解決辦法。</p><p>... <a class='more' href='http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx#continue'>繼續閱讀</a>...</p>"
},
...
]

回到網頁上會發現 Angular 自動的把 script 標籤過濾掉了,所以什麼都事情都沒發生,而且還很智慧的顯示這一段提示

意思是 Angular 幫我們從 HTML內了清除了一些有害的內容,而後面的連結則是關於 XSS 的資安文件。

替 ngFor 加上索引值

最後我們要替每一篇文章的 id 補上索引值,而 ngFor 裡面提供了一個好用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Article START-->
<article class="post" id="post{{idx}}" *ngFor="let item of atticleData; let idx = index">
<header class="post-header">
<h2 class="post-title">
<a [href]="item.href">{{ item.title }}</a>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
</header>
<section class="post-body text" [innerHTML]="item.summary">
</section>
</article>
<!-- Article END-->

只有在 ngFor 指令下才可以使用,能自行宣告一個變數 idx ,並且賦值為 index ,如此一來就能取得目前的索引值。

這時的文章 id 就會按照索引值增加了。

當然這邊也可以直接使用屬性繫結把資料內的 id 綁上去即可,只是這邊要介紹索引值的用法。

小結

總算是介紹完 Angular 的三種類型的指令了,有沒有覺得這些指令都似曾相似呢?就我來說的話肯定是有的,而大部分的用法都蠻接近的,只是細節上有不同之處。

希望能早日把 Angular 給掌握起來!

0%