title: Angular 指令 & 資料繫結
date: 2017-09-12 10:00
categories: Training
keywords:
在 Angular UI:Clarity Design System 中,透過修改 tslint.json 檔的 component-selector 屬性來讓我們可以自訂元件 selector 屬性,現在同樣的來調整 first-app 專案,將 employee 資料夾內的元件改成 emp 開頭,將 home 資料夾內的元件改成 home 開頭。
修改 tslint.json 的 component-selector 設定。  

調整相關元件的 selector 屬性。 
  
{% codeblock lang:ts %}
// 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’,
  …
{% endcodeblock %}
修改 src\app\home\home.component.html,將 app-header 改為 home-header,app-aside 改為 home-aside。  
{% codeblock home.component.html lang:html %}
{% endcodeblock %}
其實網路上已經有一些 Angular 版本的元件可以套用,不過這邊我們練習如何用簡單的方式就可以做出一個雛形。
開啟 src\app\custom-material.module.ts 加入 MdCardModule。  
{% codeblock custom-material.module.ts lang:ts %}
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 { }
{% endcodeblock %}
開啟 src\app\employee\employee.module.ts,將 CustomMaterialModule 註冊到裡面。  
{% codeblock employee.module.ts lang:ts %}
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 { }
{% endcodeblock %}
開啟 src\app\employee\calendar\calendar.component.html,加入 card 元件,以及月份切換按鈕。 
{% codeblock calendar.component.html lang:html %}
  
    
    
      2017 年 9 月
    
  
  
  
  
    
    
    
  
{% endcodeblock %}
執行專案,從瀏覽器上可以看到最外層的卡片效果。 
  
在 Angular 事件 中,我們學會 事件繫結,透過 小括號(( event )) 的方式讓前端樣板狀態變更時可以通知後端元件。
接下來我們希望畫面上的月份可以透過程式動態修改,所以我們利用 Angular 提供的另一種資料繫結模式-內嵌繫結 (interpolation),我們可以透過2個大括號({{ variable }})方式將元件所對應 ts 檔的變數嵌入到樣板上。  
若是私有(private)變數則無法存取,若變數沒有加上存取修飾詞,那預設值就是 public。
開啟 src\app\employee\calendar\calendar.component.ts,加入變數 selectedMonth,並提供方法將日期轉成文字格式給 selectedMonth。 
{% codeblock calendar.component.ts lang:ts %}
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} 月;
  }
}
{% endcodeblock %}
TypeScript 在宣告變數時必須給定型別,例如:boolean、number、string、any、…,格式是在變數後面加上冒號(
:)再加上型別,但是如果有給初始值時可以省略,因為 TypeScript 會自動依初始值來推斷變數的型別,後續程式邏輯若給予的值與型別不符時就會出現警告。
TypeScript 最終還是會編譯成 JavaScript,它主要是提供了開發時期的型別檢查。any算是比較特殊的型別,代表它可以是任意型別,當變數設成any時,我們可以視為 TypeScript 就不會做檢查,當變數的值可能為不同型別時可以使用,但是宣告成any也代表失去 TypeScript 的檢核機制,建議盡量避免使用。
在 function 內宣告變數時,TypeScript 預設會建議你用const宣告,如果值會改變時則用let。
修改 calendar.component.html,套用 selectedMonth 變數。
{% codeblock calendar.component.html lang:html %}
  
    
    
      { {selectedMonth} }
    
  
  
  
  
    
    
    
  
{% endcodeblock %}
從瀏覽器上看不出改變,但是月份已經用變數取代。 
  
修改 calendar.component.html,為切換按鈕加入事件,並透過 事件繫結 綁定。
{% codeblock calendar.component.html lang:html %}
  
    
    
      { {selectedMonth} }
    
  
  
  
  
    <button md-button (click)=”getDay(-1)”>上個月
    <button md-button (click)=”getToday()”>本月
    <button md-button (click)=”getDay(1)”>下個月
  
{% endcodeblock %}
修改 calendar.component.ts 實作對應的方法。  
{% codeblock calendar.component.ts lang:ts %}
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} 月`;
  }
}
{% endcodeblock %}
ngOnInit是 Angular 生命週期 衍生的事件,它會發生在第一次繫結資料發生變動(ngOnChanges)之後。
從瀏覽器來看,月份切換已經有效果了 
  
開啟 src\app\custom-material.module.ts 加入 MdGridListModule。  
{% codeblock custom-material.module.ts lang:ts %}
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 { }
{% endcodeblock %}
修改 calendar.component.html,加入 Grid List。
{% codeblock calendar.component.html lang:html %}
  
    
    
      { {selectedMonth} }
    
  
  
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
  
  
    <button md-button (click)=”getDay(-1)”>上個月
    <button md-button (click)=”getToday()”>本月
    <button md-button (click)=”getDay(1)”>下個月
  
{% endcodeblock %}
從瀏覽器來看,行事曆的效果已經出現了 
  
從API文件來看,MdGridListModule 與 HTML 的 Table 功能類似。
MdGridList 提供一個cols的屬性,當內容項目-MdGridTile 超過其數值時就會自動換行,我們設定7是因為一個禮拜有7天。
MdGridTile 則提供rowspan、colspan可以設定所占用的 列數 與 欄位數,這邊因為都是占用1單位,所以不設定。
Angular 提供一個樣板指令-*ngFor,讓我們可以依照陣列物件重複產生樣板語法,同時將資料嵌入到樣板內。
修改 calendar.component.ts,加入星期的陣列變數-dayHeaders。  
{% codeblock calendar.component.ts lang:ts %}
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() { }
  …
{% endcodeblock %}
修改 calendar.component.html,透過 ngFor 指令來巡覽 dayHeaders 陣列,並宣告一個 變數(header) 來承接其值,最後再透過*內嵌繫結**將 header 嵌入到樣板內。
{% codeblock calendar.component.html lang:html %}
  
    
    
      { {selectedMonth} }
    
  
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders”>
        { {header} }
      
      
      …
{% endcodeblock %}  
從瀏覽器上看不出改變,但是星期已經用變數取代。 
  
*ngFor本身還提供下列變數可使用:index(number):表示目前資料在陣列中的索引值。first(boolean):表示目前資料是否為第一筆。last(boolean):表示目前資料是否為最後一筆。even(boolean):表示目前資料的索引值是否為偶數筆。odd(boolean):表示目前資料的索引值是否為奇數筆。
但是上述變數不可直接使用,必須另行宣告變數來承接。
例如:我們宣告一個變數i來承接index索引值,並將i嵌入到星期變數前面。
Angular 提供一個類似 style tag 的指令-[ngStyle],最大的差異就是 [ngStyle] 所設定CSS的屬性值可以是變數、方法,也就是我們可以動態改變值。
修改 calendar.component.ts,加入星期的顏色陣列變數-dayColors。  
{% codeblock calendar.component.ts lang:ts %}
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() { }
  …
{% endcodeblock %}
開啟 calendar.component.html,加入 ngStyle 將文字顏色來源指向 dayColors。
{% codeblock calendar.component.html lang:html %}
  
    
    
      { {selectedMonth} }
    
  
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders; let i = index”
        [ngStyle]=”{‘color’: dayColors[i]}”>
        { {header} }
      
      
      …
{% endcodeblock %}
從瀏覽器上可以看到星期日變成紅色、星期六變成綠色。
接下來就是將日期改為動態產生,開啟 calendar.component.ts,新增 days 陣列變數,在 getDay(addDMonth) 方法內將選取月份的日期與星期都填入 days 內。  
{% codeblock calendar.component.ts lang:ts %}
…
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} 月`;
  }
}
{% endcodeblock %}
開啟 calendar.component.html,加入 ngStyle 將文字顏色來源指向 dayColors。
{% codeblock calendar.component.html lang:html %}
…
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders; let i = index”
        [ngStyle]=”{‘color’: dayColors[i]}”>
        {{header}}
      
      <md-grid-tile *ngFor=”let item of days”
        [ngStyle]=”{‘color’: dayColors[item.week]}”>
        {{item.day}}
      
    
  …
{% endcodeblock %}
從瀏覽器上可以看到日期為星期日與星期六的顏色都改變了,但是位置不對,2017/9/1 應該是禮拜五。 
  
上面提到的 內嵌繫結 可以將後端元件類別變數輸出到前端樣板,但是它最終會轉換成文字,如果我們需要繫結對象是樣板元素的屬性時可能就會不適用,因此 Angular 提供另一種資料繫結模式-屬性繫結,我們可以在目標屬性上加上 中括號([ property ]) 來表示資料來源是來自元件類別。  
屬性繫結 的資料如果是字串,可以在值外面加上單引號來表示值為字串。
我們在每月1號的 md-grid-tile 前面再插入一個 md-grid-tile,並透過 colspan 屬性將1號推擠到對應的星期。
開啟 calendar.component.ts,新增 lastMonth_colspan 變數,在 getDay(addDMonth) 方法內計算需要偏移的天數。  
{% codeblock calendar.component.ts lang:ts %}
…
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 = [];
...
{% endcodeblock %}
開啟 calendar.component.html,加入一個 md-grid-tile,並透過屬性繫結將 colspan 屬性繫結至 lastMonth_colspan 變數。
{% codeblock calendar.component.html lang:html %}
…
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders; let i = index”
        [ngStyle]=”{‘color’: dayColors[i]}”>
        {{header}}
      
      <md-grid-tile [colspan]=”lastMonth_colspan”>
      <md-grid-tile *ngFor=”let item of days”
        [ngStyle]=”{‘color’: dayColors[item.week]}”>
        {{item.day}}
      
    
  …
{% endcodeblock %}
從瀏覽器上看起來應該都是正常。 
  
我們切換到下個月(2017/10)問題就出現了,原本 2017/10/1 應該是星期日,但是卻被移到星期六。
當 lastMonth_colspan = 0 時表示不需要推移,因此應該移除日期前的 md-grid-tile,Angular 提供一個便捷的樣板指令-*ngFor,當後面的判斷式為 true 時該樣板元素會顯示,當值為 false 時該樣板元素會被移除。
開啟 calendar.component.html,添加 *ngFor 判斷式。
{% codeblock calendar.component.html lang:html %}
…
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders; let i = index”
        [ngStyle]=”{‘color’: dayColors[i]}”>
        {{header}}
      
      <md-grid-tile [colspan]=”lastMonth_colspan” *ngIf=”lastMonth_colspan”>
      …
{% endcodeblock %}
從瀏覽器上看起來 2017/10 月份行事曆應該就正常。
最後我們在 src\app\employee\calendar\calendar.component.scss 加上一點 CSS 樣式,並套用到 calendar.component.html。  
{% codeblock calendar.component.scss lang:scss %}
.day {
  border: solid 1px DimGray;
  .text {
    position: absolute;
    top: 4px;
    right: 8px;
  }
}
{% endcodeblock %}
{% codeblock calendar.component.html lang:html %}
  
    
    
      {{selectedMonth}}
    
  
  
    
      <md-grid-tile *ngFor=”let header of dayHeaders; let i = index”
        [ngStyle]=”{‘color’: dayColors[i]}”>
        {{header}}
      
      <md-grid-tile [colspan]=”lastMonth_colspan” *ngIf=”lastMonth_colspan”>
      <md-grid-tile *ngFor=”let item of days”
        [ngStyle]=”{‘color’: dayColors[item.week]}” class=”day”>
        {{item.day}}
      
    
  
  
    <button md-button (click)=”getDay(-1)”>上個月
    <button md-button (click)=”getToday()”>本月
    <button md-button (click)=”getDay(1)”>下個月
  
{% endcodeblock %}
執行專案,從瀏覽器上操作應該都正常了。 
  
{% img /images/download.png 36 %}first-app_2017-09-12.zip