[前端框架] Angular - 介紹與安裝

Angular 通常是指 Angular 2+以上的版本(與早期的 Angular 1 版本完全不同,又稱呼第一代叫 AngularJS),是由 AngularJS 團隊於 2016 年重新改寫並由 Google 所主導開發的 JavaScript 框架,與 React 相同都是採用元件 Component-based 來進行觀念導向設計,不像 VueJS 採用 MVVM(Model 資料管理、View 畫面顯示、ViewModal 溝通橋梁)觀念去區分細節,而是整個融合在 Component 整個零件內。

Angular 對於學習難度上高於其他框架,且在國內市場比例低於 React 與 Vue。相對來說在薪資上也比較不會被競爭而拉低,風險考量就在於未來市場上如何走向發展。隨著 TypeScript 的市場需求度越來越高,也許學會 Angular 不是一個壞事。以及 Angular 對於版本升級有很強烈的向後相容性。不會再因為框架的版本變化而影響原專案的開發維護難題。因此學 Angular 挑最新版的學就好了(也就是 Angular 2+)。

安裝環境

這裡提供一些可執行 Angular 的工作環境介紹。

stackblitz.com

可線上直接執行開發框架的專案平台 stackblitz.com,不需要本地安裝就能執行編譯。

Angular CLI

你可以使用 Angular CLI 來建立專案,產生應用和函式庫程式碼,以及執行各種持續開發任務,比如測試、打套件和部署。你需要一個最新版本的 node.js 比較能對應最新版的 Angular。在透過 Node 安裝最新的 CLI 之前,先嘗試更新 npm:

npm install -g npm

透過 node 指令將 CLI 安裝完成,輸入:

npm install -g @angular/cli

如果你想要更新 Angular CLI 版本,建議是先移除再安裝,但建議是去研究清楚對應的 Node.js 版本支援性:

npm uninstall -g angular-cli @angular/cli 
npm cache verify
npm install -g @angular/cli

初始化應用 app

透過指令(擇一),你能獲得指定目錄名稱的 Angular 套件與相依套件。過程中會問你幾個問題(是否要添加 Route 功能、採用哪種樣式表),隨便挑可以留最基本就好讓他執行,最後將會獲得具有 DEMO 內容的網頁。

# 嚴格模式 TypeScript 會報錯
ng new lokiFirst

# 非嚴格模式
ng new lokiFirst --no-strict

甚麼是 嚴格模式

接著到指定的目錄,這裡是 lokiFirst,可以用指令cd lokiFirst或是重新到該目錄下執行終端機指令,來啟用整個開發伺服器。以下會自動訪問到 http://localhost:4200/:

ng serve --open

這裡參數也可改用-o,都會完成後自動開啟瀏覽器。

如果要改 port 可使用以下指令:

ng serve --port 4201

現在,Angular 會自動偵測檔案,一但有變動就會自動執行更新網頁內容。

輸出生成

如果你不是透過打包工具,想要直接生成網頁伺服器的靜態應用檔案。透過指令完成,Angular 會產生 dist 目錄並將所有網頁檔案放置於此。

ng build

初次應用

你可以參考 Angular 官方提供的 課程英雄之旅,這是一個很好的初體驗帶動課程但本章節不會跟著課程做但是會加減參考內容觀念說明。

目錄說明

來到 src 目錄下,那是我們主要的應用目錄。可以發現一些東西:

透過index.html網頁檢查可發現 body 內塞了以下代碼,可發現<app-root></app-root>這是一個預設的 component 元件,所有的內容都從這裡產生出來。而每個小 component 元件又從這裡帶出來。

src/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LokiFirst</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

來到 src/app 目錄下,這裡放 Angular 的所有元件,包含我們最外層的 app Component。這是整個 Angular 主要的 Component 檔案。

  • app.component.ts — 元件的類別程式碼,這是用 TypeScript 寫的。在 Angular 都是用 ts 來編寫具備強型別的 javascript。
  • app.component.html — 元件的範本,這是用 HTML 寫的。
  • app.component.css — 元件的私有 CSS 樣式。
  • app.module.ts - 整個應用會用到的 Angular 模組,宣告在這裡就能使用 Angular 提供的模組功能。

*.spec.ts 是測試用的可以直接刪除,新手用不到。如果建立元件時可以多添加 ``參數要求不要產生該檔案,

從 ts 內可以看到,這隻檔案會匯出一個 class 並提供一個資料為 title

export class AppComponent {
title = 'lokiFirst';
}

可試著將 html 內全部刪除只留下以下內容,能發現瀏覽畫面提供了這個資料輸出。

<h1>{{title}}</h1>

這是一個所謂的 data binding 資料綁定作用。也是整個 Angular 學習上最基本的應用。接著來個簡易的 two-way data binding 雙向綁定,我們需要添加一個 input 元素並用使用 ngModel 模組來完成對資料變數可動態輸入。

<input type="text" [(ngModel)]="title">
<h1>{{title}}</h1>

雖然 ngModal 已出現在 app.module.ts 內,但畫面報錯上出現了以下資訊 Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’. 說明無髮綁定給 ngModel 模組是因為 input 的屬性沒有提供,所以 Angular 不能理解 input 內的屬性 ngModel 是要做甚麼事。由於 Angular 把模組拆得很細,如果 input 需要規劃 ngModel 功能就需要添加 FormsModule 匯入。

app.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //這裡,如果你有依賴 vscode 套件這裡不用特別寫,會自動下面產生時這裡自動填寫
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule //添加模組做匯入
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

import ... from ... 是 TypeScript 要求知道東西從哪裡來,與 Angular 無關。@NgModule{}提供給 Angular 理解有哪些模組特性使用。

接下來我們會開始學習一些重點內容

  • basics 基本知識 - 介紹什麼是元件、雙向綁定的應用等等說明
  • component 元件與 dataBinding 資料綁定 - Angular 的應用是透過元件與資料綁定來建構並透過 DOM 來生成動態畫面。
  • Directives 指令 - 我們透過 ngModel 指令來協助我們完成雙向綁定。會介紹更多內建指令資訊非常重要。
  • Services 服務與 Dependency Injection 依賴注入 - Angular 的核心特性,容易在應用內不同元件上相互通信傳遞,以及集中代碼進行狀態管理。
  • Routing 路由 - 透過路由能在不用發出 http request 方式下進行換頁(抽換大範圍 DOM)。
  • Observables 觀察者 - 在非同步工作下的使用觀念,非常重要。
  • Form 表單 - 處理用戶資料的關鍵資訊,幾乎你的工作都會用到。
  • Pipes 管道 - 譨在 template 範本上輕鬆轉換原本應該的輸出。
  • http 訪問伺服器
  • authentication 身分認證
  • Optimizations 優化與 ngModule 模組
  • Deployment 部屬

基本知識

Angular 能允許你建立單一網頁的框架,因此我們是對一個 html 文件來進行開發也就是 index.html 這個單頁檔案。而這份 index.html 如前面所說有個 app-root 元素為我們最上層的元素。詳細原理我們不用去理解,這一切都由 CLI 來幫助我們去進行關聯。

Component 元件

這裡會開始介紹元件的一些屬性以及它的結構用途。一開始從 app 這隻元件開始說起。

app.component.ts

從根元件下手,來觀察這份檔案你能看到有個裝飾器@component,裏面涵蓋了這個根元件所有重要資訊,包含了我們的 selector 選擇器指向到哪個 template 範本內元素、對應的 template 範本 (html) 在哪、元件所吃的樣式表位置。這些資訊都是為了要幫我們對 index.html 進行抽換的資訊,也就是整個app.component.html的完整內容。

app.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'lokiFirst';
}

這是根元件由 cli 提供,之後會再介紹如何建立自己的元件。而最外層的main.ts是網頁觸發 angular 的第一個腳本

第一個 Component

從 CLI 幫我們做的事情已知道會放入一個 app-root 元件到 index.html 上,而我們看到的就是這個元件下想呈現出來的 DOM 畫面。當然你可以改用自己建立的元件來替換或插入到 index.html 內。或多個元件來組合出整個應用 app,由根元件來扮演最上層的元件進行多層嵌套元件來使用。只要知道每個元件都有自己的專屬的 html 與 css,好處在於在複雜的應用 app 內可以重複使用這些相同內容的元件,使得開發的作業上更簡單些。

根元件盡可能保留下來,因為 app.modules.ts 已經都寫好指定一個元件作為根元件,除非你想手動去修改這些 CLI 已寫好的模組內容。

這裡我們嘗試自行添加一個元件並試著讓它顯示於根元件底下

建立 Component

想建立一個元件放置於根元件下面,我們會習慣用目錄的方式代表一個元建。你可以手動去建立 folder 與手動產生對應的 ts,html,css 檔案(注意檔名要對應清楚)。或者透過指令ng n g ...來建立新的元件。

# 完整指令
ng generate component myserver

# 簡化指令
ng g c myserver

# 搭配不想產生測試用的*.spec.ts
ng g c myserver --skip-tests

小技巧:關閉產生 spec.ts 的方式
如果想直接關閉這個功能,可在單一專案目錄下對 angular.json 添加參數

angular.json
{ 
"projects": {
"{PROJECT_NAME}": {
"schematics": {
"@schematics/angular:component": {
"skipTests": true
}
}
}
}
}

如果所有的專案則是在外層添加

angular.json
{ 
"schematics": {
"@schematics/angular:component": {
"skipTests": true
}
}
}

或者透過指令完成要求,等價自動幫你做上面那段代碼的事情。

# 所有專案都不要 test 檔案
ng config schematics.@schematics/angular:component.skipTests true

這裡我們透過指令完成後,會產生對應的目錄以及相關檔案,而觀察 app.module.ts 也發現 ServerComponent 也幫你寫好了(元件都會自動以大寫來表示這是一個元件對象)。觀察 NgModule 提供了四種屬性,分別是 declarations 聲明、imports 導入、providers 提供者、bootstrap 引導程序(這裡不是指 CSS 框架那個)。引導程序這裡,會告知 Angular 會使用哪個元件,也就是一開始 CLI 提供的根元件。近而產生整個 index.html 所需要的內容。所以你應該將各種元件進行嵌套到這個根元件內。因此引導程序這裡不太需要去動到他。而是將新元件被寫在 declarations 聲明處(檔案來源也記得要寫上,就是 import … from … 部分,屆時透過打包工具時才能找到這個檔案處做整合)。

@NgModule({
declarations: [
AppComponent,
ServerComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})

imports 導入這裡,通常是載入一些內建的模組,譬如這裡用到了瀏覽器與表單的模組,Angular 會將很多功能打散成模組提供使用,為了效能你需要自行指定需要的模塊來導入使用,未來會在介紹更多有哪些好用的模組。

使用 Component

讓我們回到 appComponent 的 html 部分,我們可以學 index.html 那樣將指定的標籤元素插入到這裡的 html 內。現在在 server 元件這裡的 html 已經有一些 html 代碼(你可以改只是測試用),同時從 ts 的檔案知道 CLI 幫我們在選擇器上取名為 app-server。接下來只要在根元件的 html 插入以下代碼就能形成嵌套的元件輸出使用。

app.component.html
<input type="text" [(ngModel)]="title">
<h1>{{title}}</h1>
<button class="btn btn-primary">123</button>
<hr>
<app-server></app-server>

現在你應該會看到一個 p 段落在畫面上,而這個 p 段落來自於我們稍早建立的 server 元件。

重複使用 Component

Component 可以重複地被使用在另一個元件內,這裡我們再產生一次新的 Component 取名為 servers。同時我們把單筆 server 的標籤元素寫在 servers 的 html 內。

servers.component.html
<app-server></app-server>

然後再回到根元件的 html 改成吃這個 servers 的元件

app.component.html
<input type="text" [(ngModel)]="title">
<h1>{{title}}</h1>
<button class="btn btn-primary">123</button>
<hr>
<app-servers></app-servers>

現在你應該會看到三個 p 段落在畫面上,而這個 p 段落來自於我們 servers 元件,而 servers 元件內重複了 server 元件。

templateUrl vs template

我們回到 servers 的 ts 來討論,在上面可以看到 templateUrl 代表 template 範本來自於外部連結,透過這個外部文件獲得了我們的 html 代碼。如果你的需求很低是不需要透過該元件的 html 來告知 template 範本的內容,而可以使用 template 以 string 的方式來指定內容。這是觀念上的技巧。

servers.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-servers',
// templateUrl: './servers.component.html',
template:`
<p>hello world</p>
<app-server></app-server>
`,
styleUrls: ['./servers.component.css']
})
export class ServersComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
styleUrl vs styles

在 app 元件 (app-root) 上我們編寫了 h1 這個元素,若想兌換顏色可以到 app.component.css 編寫h1{color:blue}屬性測試看看。同樣原理我們可以改用內部方式來指定但是型別為陣列指定 string。

app.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
// styleUrls: ['./app.component.css']
styles: [`
h1{
color:blue;
}
`]
})
export class AppComponent {
title = 'lokiFirst';
}

如果我們把 servers 元件的 template 內的 p 段落換成 h1,你會發現 app 元件的 style 只對 app 元件有效而對 servers 元件無感。之後再討論

selector

在@Component 內還有一個屬性為 selector,這裡的寫法其實跟 CSS 選擇器是一樣的,原範例中是這樣selector: 'app-servers'(這裡我們討論 TypeScript 語法,所以只討論 string 內容),這裡直接寫一個標籤名稱,則表示在 html 那裏的寫法是以元素來寫<app-servers></app-servers>。這我們已經清楚

如果用 CSS 選擇的觀念,我們抽換成.app-servers的方式來編寫,這代表這個元件內容要選擇到持有這個 class 名稱的元素上。

servers.component.ts
@Component({
// ...
selector: '.app-servers',
// ...
})

回到有使用這個元件的 html 上面在 app 那,我們要修改成有一個 div 他持有這個 class 名稱

app.component.html
<input type="text" [(ngModel)]="title">
<h1>{{title}}</h1>
<button class="btn btn-primary">123</button>
<hr>
<!-- 選到這個 DIV2 的 content -->
<div class="app-servers">
<!-- 這裡會塞入來自 servers 元件內容 -->
</div>

相反的我們也可以用元素屬性來找到元素對象,在 css 觀念使用[attr]來指定選擇器。

servers.component.ts
@Component({
// ...
selector: '[app-servers]',
// ...
})

回到有使用這個元件的 html 上面在 app 那修改成持有這個屬性名稱

app.component.html
<input type="text" [(ngModel)]="title">
<h1>{{title}}</h1>
<button class="btn btn-primary">123</button>
<hr>
<!-- 選到這個 DIV2 的 content -->
<div app-servers>
<!-- 這裡會塞入來自 servers 元件內容 -->
</div>

angular 不支援#id 的方式來指定,或是偽類例如 hover。通常這裡大概就這幾種方法來 selector。但絕大部分來說都只用最單純的元素選擇來操作。

Data Binding 資料綁定

資料綁定指的是一種通訊觀念,我們的程式邏輯與資料都寫在 TypeScript 裡面,而用戶是透過 template(HTML) 來獲得畫面,事後透過資料綁定進行輸出資料到畫面上。如先前出現的{{title}},就是將我們從伺服器獲取且計算後所得到的 Data 要對用戶進行顯示。而由於一開始用戶只看到 template 以傳統靜態觀念來說已經結束通信了。資料綁定就是透過另一個通信來替換 html 上的內容。

輸出 Data 到用戶的畫面上你可以選擇透過字串插植方式,也就是透過 {{ }} 的語法達到。另一種屬性綁定,透過元素的屬性名稱或某表達式達到,例如`[property=data]。

相反的方向,如果想要住戶將 Template 上的 data 傳給 TypeScript,可以透過事件的綁定來執行動作。或者兩種方向的同時雙向通信稱呼為雙向綁定 two-way binding。

String Interpolation 字串插值

這裡測試一下前面出現過的字串插值方式,值得注意畢竟是插值,因此我們也可以一個固定字串來插入到 template 內。

server.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.css']
})
export class ServerComponent {
serverId: number = 999;
serverState: string = "offline";
}
//這裡為了簡化代碼,有刪除掉預設提供的 OnInit 部分,但不影響練習
server.component.html
<p>{{'Server'}} 's id = {{serverId}} and state = {{serverState}}</p>

同前面環境,這個 server 元件被 servers 元件所使用沒有異動,此時檢查畫面上可看到,如我們期望那樣成功插入 TypeScript 的資料到畫面裡面。

Server 's id = 999 and state = offline

或者也可以插值一個 class method 來獲得一個結果,畢竟最終結果都是字串且可行的。

server.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.css']
})
export class ServerComponent {
serverId: number = 999;
serverState: string = "offline";

getServerState() {
return this.serverState;
}
}
server.component.html
<p>{{'Server'}} 's id = {{serverId}} and state = {{getServerState()}}</p>

Property Binding 屬性綁定

前面介紹的字串插植邏輯很簡單,就是想插入一個字串到 template 上面去,而這裡的屬性綁定是根據一個表達式結果決定是否要對元素屬性來做添加與否。舉例來說我們回到 servers 元件,添加一個按鈕平常所鎖住不給按的。但一經過 TypeScript 一段操作後我們會開放這個按鈕可以按,也就是我們需要透過資料通信到 template 來要求某條件成立下移除 disabled 屬性。

servers.component.html
<button type="button" disabled>Add Server</button>
<hr>
<app-server></app-server>

來到 server 的 ts 部分,先宣告一個屬性為 boolean。同時利用 constructor 建構函式他會當元件 (class) 被建立並初始化時被執行。

servers.component.ts
import { Component } from '@angular/core';

@Component({
selector: '[app-servers]',
templateUrl: './servers.component.html',
// template:`
// <h1>hello world</h1>
// <app-server></app-server>
// `,
styleUrls: ['./servers.component.css']
})
export class ServersComponent {
allowNewServer = false;
constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 3000);
}
}
//為了簡化,這裡有先移除 cli 預設給的 onInit 相關代碼

接著回到我們的 template, 若要移除 disable 屬性,需想辦法獲得一個屬性寫法為[disable]=false。額外的我們把這個結果輸出在下方 p 段落做判斷。

servers.component.html
<!-- <button type="button" disabled>Add Server</button> -->
<button
type="button"
[disabled]="!allowNewServer"
>Add Server</button>
<p>{{!allowNewServer}}</p>
<hr>
<app-server></app-server>

隨著屬性操作複雜化,你應該對屬性換行並排版

字串插值與屬性綁定

兩個的作法有不同的考量,以字串插值而言過程結果我們會獲得一個字串。如果你做為 innerText 的插入,其實 innerText 也是一個屬性能透過綁定來完成,以下不同寫法同樣結果。

servers.component.html
<!-- <p>{{!allowNewServer}}</p> -->
<p [innerText]="!allowNewServer"></p>

因此如果你只是想簡單插入一些字串就用字串插值就可以。而指令類型或是元件通常會使用屬性綁定,觀念上你不應該在屬性綁定的等號右側使用{{}}來插入字串,而是透過表達式來處理這個屬性結果的值為何。這個例子來說屬性綁定要的是一個 Boolean 值而不是字串。

Event Binding 事件綁定

接下來我們可以透過一個事件來觸發通信,透過元件的 method 來修改原屬性的值。這裡為了看出效果我們做一個字串插值來呈現資料有取得且變動。同樣使用 servers 元件來增加一個屬性與方法。

servers.component.ts
export class ServersComponent {
allowNewServer = false;
serverCreatingState='No Server Create!!';
constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 3000);
}
onCreateServer(){
this.serverCreatingState='Now Server Created!!';
}
}

現在只要想辦法讓用戶去觸發這個 onCreateServer() 就能改變屬性資料。在 servers 的 html 部分我們需要指定一個 click 事件,在 Angular 內只需要透過屬性(click)=*就能代表一個點擊事件。同時為了方便判斷我們在下方進行字串插值顯示這個資料方便觀察。

servers.component.html
<button
type="button"
[disabled]="!allowNewServer"
(click)="onCreateServer()"
>Add Server</button>

<p>{{serverCreatingState}}</p>

現在試著點選看看,就能看到用戶能透過 click 來操作 onCreateServer() 並成功去修改到元件的屬性值了。這是一個很簡單的事件綁定示範。

上面最後這樣也算是一個雙向綁定(用戶與 TypeScript 能雙向通信)。其實應該說被分為兩個動作一去一回,後續會介紹到透過 ngModal 來成為更快的雙向綁定。

透過 Event Object 傳遞與使用

使用事件綁定時,你可以嘗試從用戶那裏將整個觸發當下的事件物件,透過參數來傳遞給 TypeScript 去處理做監聽行為。要使用這個方式使用$event保留字變數來傳遞。每次的輸入都能傳遞一些數據給 TypeScript。而參數的強型別先暫時使用 Any 即可(實際上型別為 Event 型別)。

servers.component.html
<label>Server Name</label>
<input type="text"
(input)="onUpdateServerName($event)">
<hr>
<!-- ... -->
servers.component.ts
export class ServersComponent {
allowNewServer = false;
serverCreatingState='No Server Create!!';
constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 3000);
}
onCreateServer(){
this.serverCreatingState='Now Server Created!!';
}
onUpdateServerName(event:any){
console.log(event); //可獲得 event 的 Object
}
}

此時試著打一些字在該 input 位置,從 console 能發現每次的 input 動作都能已成功獲得 event 物件,更實用的做法還能從 Target 這位置找到 input 的 value 值。因此我們調整一下代碼同時設定該型別為 Event。

servers.component.ts
export class ServersComponent {
allowNewServer = false;
serverCreatingState = 'No Server Create!!';
serverName = ''; //添加一個初始屬性為空字串
constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 3000);
}
onCreateServer() {
this.serverCreatingState = 'Now Server Created!!';
}
onUpdateServerName(event: Event) {
console.log(event.target.value); //報錯,因為 Event 這個型別沒有 event.target 這樣的位置
}
}

TypeScript 沒辦法知道這個型別下的有這樣的 event.target 位置,因此我們要改讓 TypeScript 知道這是一個 HTML 的 input 元素。在 event 前綴增加一個顯式轉換。讓 TypeScript 知道 event 是一個 HTML 的 input 元素會持有這樣的位置。

servers.component.ts
onUpdateServerName(event: Event) {
console.log((<HTMLInputElement>event.target).value); //將 event.target 轉為一個 HTML Input Element 型別
}

現在可以正常抓到這個 input 的 value 了。你可以在用戶上透過字串插值來抓到這個 value。

servers.component.html
<label>Server Name</label>
<input
type="text"
(input)="onUpdateServerName($event)"
>
<p>{{serverName}}</p>
servers.component.ts
onUpdateServerName(event: Event) {
// console.log((<HTMLInputElement>event.target).value); //報錯,因為 Event 這個型別沒有 event.target 這樣的位置
this.serverName = (<HTMLInputElement>event.target).value;

記得在對 form 類型元素進行綁定時,需對app.module.ts進行 FormsModule 的 imports,這裡很順利是一開始我們就曾做過這件事。

two-way Binding 雙向綁定

前面的動作都是輸入與輸出兩個方向來達到綁定。這裡介紹更簡單的雙向綁定,透過 ngModal 來實現。為了實現差異性,保留前例的 input&click 事件與字串插值,這裡創造一個 input 指定屬性為 ngModal 來做綁定(具備雙向功能)。

ngModal 的寫法外層包覆方式,分別代表了屬性綁定[]與事件綁定()之不同語法。語法組合能夠形成雙向的通信。

servers.component.html
<input
type="text"
(input)="onUpdateServerName($event)"
>
<input
type="text"
[(ngModel)]="serverName"
>
<p>{{serverName}}</p>

試著對這兩個 input 交替輸入一些字可以發現一下特性:

  • ngModel 不需要透過元件的 Method 來操作屬性值變化,直接就能對元件屬性存取。
  • ngModal 可以真正雙向的對一個元素動態呈現目前的資料為何。而 event binding 是單向的,因此若資料異動時 input 本身不會有反應。

小節練習

設計一個元件屬性 username 可被綁定做一些用途。

  • input 輸入時下方 p 能即時出現輸入的文字 (two-way)
  • RESET 按鈕只有當有輸入時可開放使用 (event)
  • 按下 RESET 時會清空屬性文字 (Method)

Directive 指令

將類別標記為 Angular 指令的裝飾器。你可以定義自己的指令,以將自訂行為附加到 DOM 中的元素。元件就是 DOM 的一種指令,當我們將元件的 selector 放在 html 內的某處時,即指示 Angular 添加元件 template 範本內容到我們 selector 地方並由 TypeScript 去進行邏輯處理。

也有沒有 template 的指令,舉例來說 appTurnGreen 是一種可自定義的指令,通常會添加到有屬性的 selector 上。例如:

*.component.html
<p appTurnGreen>Receives a green background!</p>

指令的可以像元件的 selector 寫法那樣進行配置,或採用 CSS 的選擇器或元素名稱。要使用這個指令就是像這樣編寫 selector

*.component.ts
@Directive({
selector:'[appTurnGreen]'
})
export class TurnGreenDirective{
//...
}

之後會介紹如何自定義指令,我們先介紹一些常用的內建指令。

ngIf 判斷

就像 if 語句那樣,我們可以要求某條件下對 DOM 進行指令要求,使用 ngIf 必須前墜添加*號這是必需的,因為 ngIf 是一種結構指令,它會改變我們的 DOM 結構。舉例前面的小節練習,我們希望只有當有值才顯示 p,捨棄原本的三元字串做法。

app.component.html
<!-- <p>You input value is {{username===''?'null':username}}.</p> -->
<p *ngIf="username!==''">You input value is {{username}}.</p>

另一種作法是透過元件屬性 Boolean 來控制這個指令。

app.component.html
<label>UserName</label>
<input
type="text"
[(ngModel)]="username"
>
<button
[disabled]="username===''"
(click)="onReset()"
>RESET</button>
<!-- <p>You input value is {{username===''?'null':username}}.</p> -->
<!-- <p *ngIf="username!==''">You input value is {{username}}.</p> -->
<p *ngIf="username!==''">You input value is {{username}}.</p>
app.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
username = '';
showText = false;

onReset() {
this.showText = true;
this.username = '';
}
}

使用 else 增強 ngIf

ngIf 可以搭配 else 來操作,原本的 if 僅判斷要不要輸出這個 DOM,添加 else 可以要求改換另一組 DOM 來輸出。這裡會用到 ng-template 將我們的自訂 template 範本。注意的是自訂 template 範本需要有名字 (#前綴)且指定給 else 知道要用哪個 template 範本。

  • else 是寫在 ngIf 裡面的值,也就是雙引號的內容
  • ng-template 可以寫在其他行數位置,只要有名稱就能找到
app.component.html
<!-- <p *ngIf="username!==''">You input value is {{username}}.</p> -->
<p *ngIf="username!=='';else noMessage">You input value is {{username}}.</p>

<ng-template #noMessage>
<h5 style="color:red">You never input something there!!</h5>
</ng-template>

ngStyle 樣式

ngStyle 是一種屬性指令,本身就是拿來代表 HTML 屬性的樣式表。因此使用 ngStyle 必需要對指定元素使用屬性綁定[]方式來運作。回到一開始的 lokiFirst 專案做練習。為了差異化 server 的 p 段落不同文字結果,我們透過建構函式內的 random 來做隨機產生。

server.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.css']
})
export class ServerComponent {
serverId: number = 999;
serverState: string = "offline";

constructor() {
this.serverState = Math.random() > 0.5 ? 'online' : 'offline';
}

getServerState() {
return this.serverState;
}
}

接著將 ngStyle 綁定給我們目標元素對象。這裡先指定一個固定紅色背景。觀看效果

server.component.html
<p [ngStyle]="{background: 'red'}">
{{'Server'}} 's id = {{serverId}} and state = {{getServerState()}}
</p>

透過元件的 serverState 屬性值為何,我們可以搭配三元來做不同的輸出結果。

server.component.html
<p [ngStyle]="{
background:serverState==='online'?'green':'red'
}">
{{'Server'}} 's id = {{serverId}} and state = {{getServerState()}}
</p>

或者獨立出一個 method 為 getColor 來負責 return 獲得指定色。

server.component.html
<p [ngStyle]="{
background:getColor()
}">
{{'Server'}} 's id = {{serverId}} and state = {{getServerState()}}
</p>
server.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.css']
})
export class ServerComponent {
serverId: number = 999;
serverState: string = "offline";

constructor() {
this.serverState = Math.random() > 0.5 ? 'online' : 'offline';
}

getServerState() {
return this.serverState;
}
getColor() {
return this.serverState === 'online' ? 'green' : 'red'
}
}

ngClass CSS 類別

如果 ngStyle 等價於控制指令添加 style,那 ngClass 則是代表控制指令添加 class name,透過此指令可進行增加或刪除 class name。首先我們先針對 server 元件建立一個樣式表,為了簡化代碼直接寫在@Component的 styles 陣列內。

server.component.ts
@Component({
selector: 'app-server',
templateUrl: './server.component.html',
// styleUrls: ['./server.component.css']
styles: [`
.online{
color:white
}
`]
})

接著來到版型這裡對 p 元素添加綁定屬性 ngClass,透過元件的 serverState 屬性判定是否添加這個 class 名稱。可發現我們的 online 部分符合條件時,字呈現白色。

server.component.html
<p
[ngStyle]="{background:getColor()}"
[ngClass]="{online:serverState==='online'}"
>
{{'Server'}} 's id = {{serverId}} and state = {{getServerState()}}
</p>

ngFor 迴圈

等同於迴圈,我們可以直接透過指令來控制 DOM 執行重複的元素輸出。我們試著將 servers 原本手動靜態三組輸出改成迴圈來作業。首先需要一個初始屬性 serverList 陣列放了這三筆資料名稱,接著到 html 部分使用 *ngFor 指令綁定給 app-server 這個元素。因為 ngFor 式結構指令改變了 DOM 所以會有*前綴。

servers.component.ts
export class ServersComponent {
allowNewServer = false;
serverCreatingState = 'No Server Create!!';
serverName = '';
serverList = ['test1', 'test2', 'test3']; //一開始有三組

constructor() {
setTimeout(() => {
this.allowNewServer = true;
}, 3000);
}
onCreateServer() {
this.serverCreatingState = 'Now Server Created!!';
}
onUpdateServerName(event: Event) {
this.serverName = (<HTMLInputElement>event.target).value;
}
}
servers.component.html
<!-- ... -->
<app-server *ngFor="let item of serverList"></app-server>

for/of 的觀念同 JavaScript 一樣使用而目前我們不會拿 item 做些什麼工作。現在你的 server 元件透過元素進行迴圈而產出的。為了可以讓使用者能增加列表,我們把按鈕綁定事件允許添加到 serverList 內,對 onCreateServer() 再調整一下:

servers.component.ts
//...
onCreateServer() {
this.serverCreatingState = 'Now Server Created!!';
this.serverList.push(this.serverName);
}
//...

小節練習

  • 建立切換按鈕,使用 event click 來控制一些事情。
  • 建立段落來顯示文字為 secret password = tuna,透過 ngIf 來判斷只有在按鈕次數基數當下才會出現
  • 每按幾次按鈕會出現幾次帶數字的段落 p,使用 ngFor 來執行。起始數字 1 開始
  • 使用 ngStyle,當數字 5 開始的段落 p 會有背景。
  • 使用 ngClass,當數字 5 開始的段落 p 會有白字。

透過 ngFor 獲得 index 值

利用作業內容繼續討論,ngFor 本身執行來源的是陣列,如果想抽出陣列的 index 值是可以用分號來操作。為了好看我們改顯示文字為時間,而 new Date 是內建 js 物件不需要宣告來源就能使用。

app.component.html
<button (click)="onToggleButton()">BUTTON</button>
<p *ngIf="showOn">secret password = tuna</p>
<p
*ngFor="let item of logs;let i=index"
[ngStyle]="{background: i>=4?'green':'transparent'}"
[ngClass]="{addWhite: i>=4}"
>{{item}}</p>
app.component.ts
//...
onToggleButton() {
this.showOn = !this.showOn;
// this.logs.push(this.logs.length + 1);
this.logs.push(new Date());
}

透過 index 值拿來提供 ngColor 與 ngStyle 進行處理。

參考文獻