Angular 指令 & 資料繫結

指令 (Directive) & 資料繫結 (Binding)

Angular UI:Clarity Design System 中,透過修改 tslint.json 檔的 component-selector 屬性來讓我們可以自訂元件 selector 屬性,現在同樣的來調整 first-app 專案,將 employee 資料夾內的元件改成 emp 開頭,將 home 資料夾內的元件改成 home 開頭。
修改 tslint.jsoncomponent-selector 設定。

img
調整相關元件的 selector 屬性。
img

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
48
49
50
// src\app\employee\address-book\address-book.component.ts
@Component({
selector: 'emp-address-book',
...

// src\app\employee\calendar\calendar.component.ts
@Component({
selector: 'emp-calendar',
...

// src\app\employee\file\file.component.ts
@Component({
selector: 'emp-file',
...

// src\app\employee\leave\leave.component.ts
@Component({
selector: 'emp-leave',
...

// src\app\employee\logbook\logbook.component.ts
@Component({
selector: 'emp-logbook',
...

// src\app\employee\reimburse\reimburse.component.ts
@Component({
selector: 'emp-reimburse',
...

// src\app\employee\to-do-list\to-do-list.component.ts
@Component({
selector: 'emp-to-do-list',
...

// src\app\home\aside\aside.component.ts
@Component({
selector: 'home-aside',
...

// src\app\home\header\header.component.ts
@Component({
selector: 'home-header',
...

// src\app\home\home.component.ts
@Component({
selector: 'home',
...

修改 src\app\home\home.component.html,將 app-header 改為 home-headerapp-aside 改為 home-aside

home.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div fxFill fxLayout="column" class="layout">
<div class="header" fxLayout="row">
<home-header fxFlex (sidenavClick)="sidenav.open()"></home-header>
</div>
<div fxFlex fxLayout="row" class="main">
<div fxFlex="200px" class="aside" fxHide.xs>
<home-aside (menuClick)="sidenav.close()"></home-aside>
</div>
<md-sidenav-container fxFlex>
<md-sidenav #sidenav mode="over" class="aside" fxHide fxShow.xs>
<home-aside fxFlex="200px" (menuClick)="sidenav.close()"></home-aside>
</md-sidenav>
<div fxFlex class="content">
<router-outlet></router-outlet>
</div>
</md-sidenav-container>
</div>
</div>

編輯行事曆

其實網路上已經有一些 Angular 版本的元件可以套用,不過這邊我們練習如何用簡單的方式就可以做出一個雛形。
開啟 src\app\custom-material.module.ts 加入 MdCardModule

custom-material.module.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
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import {
MdIconModule,
MdButtonModule,
MdListModule,
MdToolbarModule,
MdTooltipModule,
MdSidenavModule,
MdCardModule
} from '@angular/material';

@NgModule({
imports: [
FlexLayoutModule,
MdIconModule,
MdButtonModule,
MdListModule,
MdToolbarModule,
MdTooltipModule,
MdSidenavModule,
MdCardModule
],
exports: [
FlexLayoutModule,
MdIconModule,
MdButtonModule,
MdListModule,
MdToolbarModule,
MdTooltipModule,
MdSidenavModule,
MdCardModule
]
})
export class CustomMaterialModule { }

開啟 src\app\employee\employee.module.ts,將 CustomMaterialModule 註冊到裡面。

employee.module.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
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarComponent } from './calendar/calendar.component';
import { AddressBookComponent } from './address-book/address-book.component';
import { LogbookComponent } from './logbook/logbook.component';
import { ToDoListComponent } from './to-do-list/to-do-list.component';
import { FileComponent } from './file/file.component';
import { LeaveComponent } from './leave/leave.component';
import { ReimburseComponent } from './reimburse/reimburse.component';
import { EmployeeRoutingModule } from './employee-routing.module';
import { CustomMaterialModule } from '../custom-material.module';

@NgModule({
imports: [
CommonModule,
EmployeeRoutingModule,
CustomMaterialModule
],
declarations: [
CalendarComponent,
AddressBookComponent,
LogbookComponent,
ToDoListComponent,
FileComponent,
LeaveComponent,
ReimburseComponent
]
})
export class EmployeeModule { }

開啟 src\app\employee\calendar\calendar.component.html,加入 card 元件,以及月份切換按鈕。

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>2017 年 9 月</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
</md-card-content>
<md-card-actions>
<button md-button>上個月</button>
<button md-button>本月</button>
<button md-button>下個月</button>
</md-card-actions>
</md-card>

執行專案,從瀏覽器上可以看到最外層的卡片效果。
img

Binding:內嵌繫結 (interpolation)

Angular 事件 中,我們學會 事件繫結,透過 小括號(( event )) 的方式讓前端樣板狀態變更時可以通知後端元件。
接下來我們希望畫面上的月份可以透過程式動態修改,所以我們利用 Angular 提供的另一種資料繫結模式-內嵌繫結 (interpolation),我們可以透過2個大括號({{ variable }})方式將元件所對應 ts 檔的變數嵌入到樣板上。

若是私有(private)變數則無法存取,若變數沒有加上存取修飾詞,那預設值就是 public。

開啟 src\app\employee\calendar\calendar.component.ts,加入變數 selectedMonth,並提供方法將日期轉成文字格式給 selectedMonth

calendar.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
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'emp-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
constructor() { }

ngOnInit() {
this.getToday();
}
getToday() {
this.selectedDay = new Date();
this.getDay();
}
getDay() {
let year = this.selectedDay.getFullYear();
let month = this.selectedDay.getMonth();
month++;
// month為13時表示隔年的1月。
if (month === 13) {
month = 1;
year++;
}
this.selectedMonth = `${year}${month} 月`;
}
}

TypeScript 在宣告變數時必須給定型別,例如:boolean、number、string、any、…,格式是在變數後面加上冒號(:)再加上型別,但是如果有給初始值時可以省略,因為 TypeScript 會自動依初始值來推斷變數的型別,後續程式邏輯若給予的值與型別不符時就會出現警告。
TypeScript 最終還是會編譯成 JavaScript,它主要是提供了開發時期的型別檢查。
any 算是比較特殊的型別,代表它可以是任意型別,當變數設成 any 時,我們可以視為 TypeScript 就不會做檢查,當變數的值可能為不同型別時可以使用,但是宣告成 any 也代表失去 TypeScript 的檢核機制,建議盡量避免使用。
在 function 內宣告變數時,TypeScript 預設會建議你用 const 宣告,如果值會改變時則用 let

修改 calendar.component.html,套用 selectedMonth 變數。

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>{ {selectedMonth} }</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
</md-card-content>
<md-card-actions>
<button md-button>上個月</button>
<button md-button>本月</button>
<button md-button>下個月</button>
</md-card-actions>
</md-card>

從瀏覽器上看不出改變,但是月份已經用變數取代。
img

加入月份切換功能

修改 calendar.component.html,為切換按鈕加入事件,並透過 事件繫結 綁定。

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>{ {selectedMonth} }</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
</md-card-content>
<md-card-actions>
<button md-button (click)="getDay(-1)">上個月</button>
<button md-button (click)="getToday()">本月</button>
<button md-button (click)="getDay(1)">下個月</button>
</md-card-actions>
</md-card>

修改 calendar.component.ts 實作對應的方法。

calendar.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
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'emp-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
constructor() { }

ngOnInit() {
this.getToday();
}
getToday() {
this.selectedDay = new Date();
this.getDay(0);
}

getDay(addDMonth: number) {
let year = this.selectedDay.getFullYear();
let month = this.selectedDay.getMonth() + addDMonth;
const dt = new Date(year, month, 1);
year = dt.getFullYear();
month = dt.getMonth();

this.selectedDay = new Date(year, month, 1);
month++;
// month為13時表示隔年的1月。
if (month === 13) {
month = 1;
year++;
}
this.selectedMonth = `${year}${month} 月`;
}
}

ngOnInit 是 Angular 生命週期 衍生的事件,它會發生在第一次繫結資料發生變動(ngOnChanges)之後。
img

從瀏覽器來看,月份切換已經有效果了
img

加入 Grid List

開啟 src\app\custom-material.module.ts 加入 MdGridListModule

custom-material.module.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
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import {
MdIconModule,
...
MdCardModule,
MdGridListModule
} from '@angular/material';

@NgModule({
imports: [
FlexLayoutModule,
...
MdCardModule,
MdGridListModule
],
exports: [
FlexLayoutModule,
...
MdCardModule,
MdGridListModule
]
})
export class CustomMaterialModule { }

修改 calendar.component.html,加入 Grid List。

calendar.component.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>{ {selectedMonth} }</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile></md-grid-tile>
<md-grid-tile>1</md-grid-tile>
<md-grid-tile>2</md-grid-tile>
<md-grid-tile>3</md-grid-tile>
<md-grid-tile>4</md-grid-tile>
<md-grid-tile>5</md-grid-tile>
<md-grid-tile>6</md-grid-tile>
<md-grid-tile>7</md-grid-tile>
<md-grid-tile>8</md-grid-tile>
<md-grid-tile>9</md-grid-tile>
<md-grid-tile>10</md-grid-tile>
<md-grid-tile>11</md-grid-tile>
<md-grid-tile>12</md-grid-tile>
<md-grid-tile>13</md-grid-tile>
<md-grid-tile>14</md-grid-tile>
<md-grid-tile>15</md-grid-tile>
<md-grid-tile>16</md-grid-tile>
<md-grid-tile>17</md-grid-tile>
<md-grid-tile>18</md-grid-tile>
<md-grid-tile>19</md-grid-tile>
<md-grid-tile>20</md-grid-tile>
<md-grid-tile>21</md-grid-tile>
<md-grid-tile>22</md-grid-tile>
<md-grid-tile>23</md-grid-tile>
<md-grid-tile>24</md-grid-tile>
<md-grid-tile>25</md-grid-tile>
<md-grid-tile>26</md-grid-tile>
<md-grid-tile>27</md-grid-tile>
<md-grid-tile>28</md-grid-tile>
<md-grid-tile>29</md-grid-tile>
<md-grid-tile>30</md-grid-tile>
<md-grid-tile>31</md-grid-tile>
</md-grid-list>
</md-card-content>
<md-card-actions>
<button md-button (click)="getDay(-1)">上個月</button>
<button md-button (click)="getToday()">本月</button>
<button md-button (click)="getDay(1)">下個月</button>
</md-card-actions>
</md-card>

從瀏覽器來看,行事曆的效果已經出現了
img

API文件來看,MdGridListModule 與 HTML 的 Table 功能類似。
MdGridList 提供一個 cols 的屬性,當內容項目-MdGridTile 超過其數值時就會自動換行,我們設定 7 是因為一個禮拜有7天。
MdGridTile 則提供 rowspancolspan 可以設定所占用的 列數欄位數,這邊因為都是占用1單位,所以不設定。

Directive:*ngFor

Angular 提供一個樣板指令-*ngFor,讓我們可以依照陣列物件重複產生樣板語法,同時將資料嵌入到樣板內。
修改 calendar.component.ts,加入星期的陣列變數-dayHeaders

calendar.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'emp-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
constructor() { }
...

修改 calendar.component.html,透過 ngFor 指令來巡覽 dayHeaders 陣列,並宣告一個 變數(header) 來承接其值,最後再透過*內嵌繫結**將 header 嵌入到樣板內。

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>{ {selectedMonth} }</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders">
<h3>{ {header} }</h3>
</md-grid-tile>
<md-grid-tile>1</md-grid-tile>
...

從瀏覽器上看不出改變,但是星期已經用變數取代。
img

*ngFor 本身還提供下列變數可使用:
index (number):表示目前資料在陣列中的索引值。
first (boolean):表示目前資料是否為第一筆。
last (boolean):表示目前資料是否為最後一筆。
even (boolean):表示目前資料的索引值是否為偶數筆。
odd (boolean):表示目前資料的索引值是否為奇數筆。
但是上述變數不可直接使用,必須另行宣告變數來承接。
例如:我們宣告一個變數 i 來承接 index 索引值,並將 i 嵌入到星期變數前面。
img
img

Directive:NgStyle

Angular 提供一個類似 style tag 的指令-[ngStyle],最大的差異就是 [ngStyle] 所設定CSS的屬性值可以是變數、方法,也就是我們可以動態改變值。
修改 calendar.component.ts,加入星期的顏色陣列變數-dayColors

calendar.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'emp-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
dayColors = ['red', 'white', 'white', 'white', 'white', 'white', 'green'];
constructor() { }
...

開啟 calendar.component.html,加入 ngStyle 將文字顏色來源指向 dayColors

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1>{ {selectedMonth} }</h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders; let i = index"
[ngStyle]="{'color': dayColors[i]}">
<h3>{ {header} }</h3>
</md-grid-tile>
<md-grid-tile>1</md-grid-tile>
...

從瀏覽器上可以看到星期日變成紅色、星期六變成綠色。
img
接下來就是將日期改為動態產生,開啟 calendar.component.ts,新增 days 陣列變數,在 getDay(addDMonth) 方法內將選取月份的日期與星期都填入 days 內。

calendar.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
...
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
dayColors = ['red', 'white', 'white', 'white', 'white', 'white', 'green'];
days = [];
constructor() { }
...
getDay(addDMonth: number) {
let year = this.selectedDay.getFullYear();
let month = this.selectedDay.getMonth() + addDMonth;
const dt = new Date(year, month, 1);
year = dt.getFullYear();
month = dt.getMonth();

const _days = [];
for (let day = 1; day <= 31; day++) {
const time = new Date(year, month, day);
if (time.getMonth() > month) {
break;
}
const d: any = {
day: day,
week: time.getDay()
};
_days.push(d);
}
this.days = [..._days];

this.selectedDay = new Date(year, month, 1);
month++;
// month為13時表示隔年的1月。
if (month === 13) {
month = 1;
year++;
}
this.selectedMonth = `${year}${month} 月`;
}
}

開啟 calendar.component.html,加入 ngStyle 將文字顏色來源指向 dayColors

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
...
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders; let i = index"
[ngStyle]="{'color': dayColors[i]}">
<h3></h3>
</md-grid-tile>
<md-grid-tile *ngFor="let item of days"
[ngStyle]="{'color': dayColors[item.week]}">
<span></span>
</md-grid-tile>
</md-grid-list>
...

從瀏覽器上可以看到日期為星期日與星期六的顏色都改變了,但是位置不對,2017/9/1 應該是禮拜五。
img

Binding:屬性繫結 (Property Binding)

上面提到的 內嵌繫結 可以將後端元件類別變數輸出到前端樣板,但是它最終會轉換成文字,如果我們需要繫結對象是樣板元素的屬性時可能就會不適用,因此 Angular 提供另一種資料繫結模式-屬性繫結,我們可以在目標屬性上加上 中括號([ property ]) 來表示資料來源是來自元件類別。

屬性繫結 的資料如果是字串,可以在值外面加上單引號來表示值為字串。

我們在每月1號的 md-grid-tile 前面再插入一個 md-grid-tile,並透過 colspan 屬性將1號推擠到對應的星期。
開啟 calendar.component.ts,新增 lastMonth_colspan 變數,在 getDay(addDMonth) 方法內計算需要偏移的天數。

calendar.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
export class CalendarComponent implements OnInit {
selectedDay: Date;
selectedMonth = '';
dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
dayColors = ['red', 'white', 'white', 'white', 'white', 'white', 'green'];
days = [];
lastMonth_colspan = 0;
constructor() { }
...
getDay(addDMonth: number) {
let year = this.selectedDay.getFullYear();
let month = this.selectedDay.getMonth() + addDMonth;
const dt = new Date(year, month, 1);
year = dt.getFullYear();
month = dt.getMonth();

this.lastMonth_colspan = new Date(year, month, 1).getDay();
const _days = [];
...

開啟 calendar.component.html,加入一個 md-grid-tile,並透過屬性繫結colspan 屬性繫結至 lastMonth_colspan 變數。

calendar.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders; let i = index"
[ngStyle]="{'color': dayColors[i]}">
<h3></h3>
</md-grid-tile>
<md-grid-tile [colspan]="lastMonth_colspan"></md-grid-tile>
<md-grid-tile *ngFor="let item of days"
[ngStyle]="{'color': dayColors[item.week]}">
<span></span>
</md-grid-tile>
</md-grid-list>
...

從瀏覽器上看起來應該都是正常。
img

Directive:*ngFor

我們切換到下個月(2017/10)問題就出現了,原本 2017/10/1 應該是星期日,但是卻被移到星期六。
img
lastMonth_colspan = 0 時表示不需要推移,因此應該移除日期前的 md-grid-tile,Angular 提供一個便捷的樣板指令-*ngFor,當後面的判斷式為 true 時該樣板元素會顯示,當值為 false 時該樣板元素會被移除。
開啟 calendar.component.html,添加 *ngFor 判斷式。

calendar.component.html
1
2
3
4
5
6
7
8
9
...
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders; let i = index"
[ngStyle]="{'color': dayColors[i]}">
<h3></h3>
</md-grid-tile>
<md-grid-tile [colspan]="lastMonth_colspan" *ngIf="lastMonth_colspan"></md-grid-tile>
...

從瀏覽器上看起來 2017/10 月份行事曆應該就正常。
img
最後我們在 src\app\employee\calendar\calendar.component.scss 加上一點 CSS 樣式,並套用到 calendar.component.html

calendar.component.scss
1
2
3
4
5
6
7
8
9
.day {
border: solid 1px DimGray;
.text {
position: absolute;
top: 4px;
right: 8px;
}
}

calendar.component.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
<md-card>
<md-card-header>
<md-card-title>行事曆</md-card-title>
<md-card-subtitle>
<h1></h1>
</md-card-subtitle>
</md-card-header>
<md-card-content>
<md-grid-list cols="7" rowHeight="68px">
<md-grid-tile *ngFor="let header of dayHeaders; let i = index"
[ngStyle]="{'color': dayColors[i]}">
<h3></h3>
</md-grid-tile>
<md-grid-tile [colspan]="lastMonth_colspan" *ngIf="lastMonth_colspan"></md-grid-tile>
<md-grid-tile *ngFor="let item of days"
[ngStyle]="{'color': dayColors[item.week]}" class="day">
<span class="text"></span>
</md-grid-tile>
</md-grid-list>
</md-card-content>
<md-card-actions>
<button md-button (click)="getDay(-1)">上個月</button>
<button md-button (click)="getToday()">本月</button>
<button md-button (click)="getDay(1)">下個月</button>
</md-card-actions>
</md-card>

執行專案,從瀏覽器上操作應該都正常了。
img

[**first-app_2017-09-12.zip**](/uploads/first-app_2017-09-12.zip)