[功能介紹-10] Reactive Forms (Model-Driven Forms) 

Template-Driven Forms與Model-Driven Forms的比較

Reactive forms

Reactive forms的驗證大多是直接寫在controller裡的,會是一個明確的、非UI的data flowing。
Reactive forms的reactive patterns可以讓測試與驗證更加簡單。

使用Reactive forms可以用一個樹狀的控制物件來binding到表單template的元件上,這讓所有驗證的程式碼都集中在一起,方便維護與管理,在撰寫單元測試時也會較為容易。

使用Model-Driven Forms也較符合reactive programming的概念(延伸閱讀:Functional Reactive Programming 的入門心得

Template-driven forms

Template-driven forms是將組件驗證控制的功能寫在像是<input>或<select>的標籤內,並利用ngModel來確認是否輸入了合法的內容。
使用表單驅動驗證不需要自己創建control objects,因為angular已經為我們建好了。
ngModel會處理使用者改變與輸入表單的事件,並更新ngModel裡面的可變數據,讓我們可以去處理後續的事。
也因此ngModel並不是ReactiveFormsModule的一部份。

這代表著使用表單驅動驗證,我們需要撰寫的程式碼更少。
但是如果我們的表單需要很複雜的驗證步驟並且要顯示很多不同的錯誤訊息時,使用表單驅動驗證會使事情變得更複雜並難以維護。

最大的差異,同步與非同步

Reactive forms是同步的而Template-driven forms為非同步處理,是這兩者間最大的差異。

對Reactive forms來說,所有表單的資料是在code裡以tree的方式來呈現,所以在任一個節點可以取得其他表單的資料,並且這些資料是即時同步被更新的。我們也可以在使用者修改了某個input的值時,去為使用者自動update另一個input內的預設值,這是因為所有資料都是隨時可取得的。

Template-driven forms在每一個表單元件各自透過directive委派檢查的功能,為了避免檢查後修改而造成檢查失效的問題,directive會在更多的時後去檢查輸入的值的正確性,因此並沒有辦法立即的得到回應,而需要一小段的時間才有辦法得到使用者輸入的值是否合法的回應。這會讓我們在撰寫單元測試時更加複雜,我們會需要利用setTimeout去讓取得的檢查結果是正確的

使用Reactive Forms

Reactive Forms的功能封裝在ReactiveFormsModule中,和FormsModule同樣在@angular/forms之下。
如果要使用Reactive Forms需要使用下面的程式碼

import { ReactiveFormsModule } from '@angular/forms';

Reactive Forms的四個重要成員

  • AbstractControl:AbstractControl是FormControl、FormGroup、FormArray這三個實例表單類的抽象基類。它提供了他們的通用行為以及屬性,例如observable。
  • FormControl:在單個表單元件中檢查值並驗證狀態(比如input、select等等)。
  • FormGroup一組值與驗證狀態(FormControl),其屬性包含了它們的子控件。例如一個form表單就是一個FormGroup。
  • FormArray:用索引的方式去追蹤檢查表單的驗證狀態。

Add a FormGroup

首先要新增FormGroup所需使用的類別

import { Component }              from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

然後創建這個Group,並定義裡面的驗證元素

export class HeroDetailComponent2 {
  heroForm = new FormGroup ({
    name: new FormControl()
  });
}

接著,在template裡面的form裡指定這個form要使用heroForm來做表單驗證,並且在input裡面指定他的formControlName

<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form &#91;formGroup&#93;="heroForm" novalidate>
  <div class="form-group">
    <label class="center-block">Name:
      <input class="form-control" formControlName="name">
    </label>
  </div>
</form>

form標籤下的novalidate屬性,是為了要防止瀏覽器自己執行native的驗證。

<form &#91;formGroup&#93;="heroForm" novalidate>

[formGroup]=”heroForm”則是將template內的form元件與controller裡所創的formGroup做關連

<input class="form-control" formControlName="name">

這個則是將input與formGroup下名為name的formControl做關連。

註:bootstrap的form-group以及form-control與angular完全無關。下面是bootstrap為我們設計的form表單樣式範例,但是這只是css,沒辦法讓組件與控制器結合。

<form>
  <div class="form-group">
    <label for="formGroupExampleInput">Example label</label>
    <input type="text" class="form-control" id="formGroupExampleInput" placeholder="Example input">
  </div>
  <div class="form-group">
    <label for="formGroupExampleInput2">Another label</label>
    <input type="text" class="form-control" id="formGroupExampleInput2" placeholder="Another input">
  </div>
</form>

Introduction to FormBuilder

FormBuilder可以減少我們在創建formGroup時有太多重覆的定義,要使用要先import必要的檔案

import { Component }              from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

使用formBuild大致要做的事如下:

  • 宣告heroForm為FormGroup
  • 在初始化元件時inject FormBuilder
  • 創建form控件時需要另外去呼叫函數createForm,使用注入的FormBuilder來創建formControl
  • 在創建formGroup時,用this.fb.group來宣告這個formGroup裡所有的formControl。
export class HeroDetailComponent3 {
  heroForm: FormGroup; // <--- 宣告heroForm為FormGroup

  constructor(private fb: FormBuilder) { // <--- 注入FormBuilder
    this.createForm();
  }

  createForm() {
    this.heroForm = this.fb.group({
      name: '', // <--- 建立一個名為name,預設值為''的formControl
    });
  }
}&#91;/code&#93;
formBuild的宣告方式如上,name控件由其初始數據值(一個空字符串)定義。
<h3>使用驗證器</h3>
首先要先import該驗證器
[code lang="js"]import { Component }                          from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

然後在建立formControl時指定使用該驗證器

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
});

Nested FormGroups

有時我們在做地址輸入框時,會有如國家、區、鄉、市、街、郵遞區號等不同的輸入欄位,但他們應該是一個group,這時候就可以用nested formGroup。透過這樣的結構層次,可以讓我們在追蹤表格狀態更為容易清楚。

export class HeroDetailComponent5 {
  heroForm: FormGroup;
  states = states;

  constructor(private fb: FormBuilder) {
    this.createForm();
  }

  createForm() {
    this.heroForm = this.fb.group({ // <-- the parent FormGroup
      name: &#91;'', Validators.required &#93;,
      address: this.fb.group({ // <-- the child FormGroup
        street: '',
        city: '',
        state: '',
        zip: ''
      }),
      power: '',
      sidekick: ''
    });
  }
}&#91;/code&#93;
我們用一個div將整個地址的區塊包起來,並用formGroupName="address"來與heroForm裡的address做連結
&#91;code lang="html"&#93;<div formGroupName="address" class="well well-lg">
  <h4>Secret Lair</h4>
  <div class="form-group">
    <label class="center-block">Street:
      <input class="form-control" formControlName="street">
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">City:
      <input class="form-control" formControlName="city">
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">State:
      <select class="form-control" formControlName="state">
        <option *ngFor="let state of states" &#91;value&#93;="state">{{state}}</option>
      </select>
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">Zip Code:
      <input class="form-control" formControlName="zip">
    </label>
  </div>
</div>

檢視FormControl裡的屬性

我們可以用下面的方式來將formControl裡可使用的屬性都印出來

<p>Form value: {{ heroForm.value | json }}</p>

基本上會有下面四個屬性可以讓我們使用

屬性 描述
myControl.value FormControl使用者輸入的值
myControl.status 這個FormControl的驗證結果. 可能的值有: VALIDINVALIDPENDING, or DISABLED.
myControl.pristine 使用者是否有在UI上更動過這個元素,假如沒有的話會是true。相反的屬性為myControl.dirty.
myControl.untouched 使用者尚未輸入並且從未觸發過blur event時為true。相反的屬性為 myControl.touched.

The data model and the form model

以往我們在創建資料類型時是像這樣子的

export class Hero {
  id = 0;
  name = '';
  addresses: Address[];
}

export class Address {
  street = '';
  city   = '';
  state  = '';
  zip    = '';
}

但是我們在創建formGroup是這樣子的

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group({
    street: '',
    city: '',
    state: '',
    zip: ''
  }),
  power: '',
  sidekick: ''
});

我們可以直接利用class來創建formControl

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group(new Address()), // <-- a FormGroup with a new address
  power: '',
  sidekick: ''
});&#91;/code&#93;
<h3>使用setValue和patchValue為表單填入初始值</h3>
在上面data model和form model的介紹範例中,可以看到Hero與formGroup建立heroForm模型有兩個顯著的區別:
<ul>
	<li>Hero class有id,formGroup沒有。</li>
	<li>Hero class的地址是一個陣列</li>
</ul>
但是我們可以利用setValue來更簡單的將一個class的資料填進表單中。
[code lang="js"]this.heroForm.setValue({
  name:    this.hero.name,
  address: this.hero.addresses[0] || new Address()//這是因為form元件只能顯示一個地址,如果class內容沒有值時,要預設新建立一個Address物件
});

也可以使用patchValue來將單一的值填入表單裡

this.heroForm.patchValue({
  name: this.hero.name
});

如果我們要做一個修改hero資料的列表,當點下某個hero時就可以修改該hero的資料

<nav>
  <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>

<div *ngIf="selectedHero">
  <app-hero-detail &#91;hero&#93;="selectedHero"></app-hero-detail>
</div>

然後在controller裡面去監聽ngOnChange事件並且用setValue來設定要修改的值

ngOnChanges() {
  this.heroForm.reset({
    name: this.hero.name,
    address: this.hero.addresses[0] || new Address()
  });
}

會需要使用reset是為了要清除前一個hero的資料

使用FormArray來呈現一個FormGroups

如果一個hero可能需要有多組的地址時,就會需要使用formArray。
原本我們是這樣定義Address的

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group(new Address()), // <-- a FormGroup with a new address
  power: '',
  sidekick: ''
});&#91;/code&#93;
使用formArray則變成這樣
&#91;code lang="js"&#93;this.heroForm = this.fb.group({
  name: &#91;'', Validators.required &#93;,
  secretLairs: this.fb.array(&#91;&#93;), // <-- secretLairs as an empty FormArray
  power: '',
  sidekick: ''
});&#91;/code&#93;
可以用下面的function將很多組的address設定進去formArray成為預設值
&#91;code lang="js"&#93;setAddresses(addresses: Address&#91;&#93;) {
  const addressFGs = addresses.map(address => this.fb.group(address));
  const addressFormArray = this.fb.array(addressFGs);
  this.heroForm.setControl('secretLairs', addressFormArray);
}

要取得formArray可以撰寫下面的方法

get secretLairs(): FormArray {
  return this.heroForm.get('secretLairs') as FormArray;
};

而顯示方式如下:

<div formArrayName="secretLairs" class="well well-lg">
  <div *ngFor="let address of secretLairs.controls; let i=index" &#91;formGroupName&#93;="i" >
    <!-- The repeated address template -->
  </div>
</div>

完整內容如下:

<div formArrayName="secretLairs" class="well well-lg">
  <div *ngFor="let address of secretLairs.controls; let i=index" &#91;formGroupName&#93;="i" >
    <!-- The repeated address template -->
    <h4>Address #{{i + 1}}</h4>
    <div style="margin-left: 1em;">
      <div class="form-group">
        <label class="center-block">Street:
          <input class="form-control" formControlName="street">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">City:
          <input class="form-control" formControlName="city">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">State:
          <select class="form-control" formControlName="state">
            <option *ngFor="let state of states" &#91;value&#93;="state">{{state}}</option>
          </select>
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">Zip Code:
          <input class="form-control" formControlName="zip">
        </label>
      </div>
    </div>
    <br />
    <!-- End of the repeated address template -->
  </div>
</div>

要為這個hero新增一個地址可以用下面這個方法

addLair() {
  this.secretLairs.push(this.fb.group(new Address()));
}

按下增加地址按鈕時呼叫這個方法

<button (click)="addLair()" type="button">Add a Secret Lair</button>

將formControl的資料用深層複製存回class裡的方法
prepareSaveHero(): Hero {
const formModel = this.heroForm.value;

// deep copy of form model lairs
const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
(address: Address) => Object.assign({}, address)
);

// return new `Hero` object containing a combination of original hero value(s)
// and deep copies of changed form model values
const saveHero: Hero = {
id: this.hero.id,
name: formModel.name as string,
// addresses: formModel.secretLairs // <-- bad! addresses: secretLairsDeepCopy }; return saveHero; }[/code]

參考資料

[功能介紹-9] Template-Driven Forms

使用Event由template表單傳送資料給Component

利用事件的$event去傳送相關資訊給component

<input (keyup)="onKey($event)">
  <p>{{values}}</p>

接收則可以透過event.target去存取該htmlInputElement的資料(詳細資料請見此
下面的範例能將value存至一個變數內且顯示在頁面上:

export class KeyUpComponent_v1 {
  values = '';

  onKey(event: any) { // without type info
    this.values += event.target.value + ' | ';
  }
}

模版驅動的表單

使用模版驅動的表單需要在app.module.ts裡面宣告我們要使用FormsModule這個library,宣告方式如下

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';//使用模版表單驅動需要import這個模組

import { AppComponent }  from './app.component';
import { HeroFormComponent } from './hero-form/hero-form.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule//在@ngModule裡去加上這個模組,這使應用程序可以訪問所有模板驅動的表單功能,包括。@NgModule、ngModel
  ],
  declarations: [
    AppComponent,
    HeroFormComponent
  ],
  providers: [],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

使用ngModel對input的值與controller裡的變數做雙向繫結

下面這段code最主要要看的是[(ngModel)]="model.name",用這個標籤會自動將使用者輸入此input的值雙向綁定到model.name上面。

<input type="text" class="form-control" id="name"
       required
       &#91;(ngModel)&#93;="model.name" name="name">
你所輸入的資料是: {{model.name}}

然後在component也要有model這個屬性才能夠被綁定

export class HeroFormComponent {
  model ={};
}

成果如下:

ngModel會附加的class

狀態(State ) Class if true Class if false
被點擊接觸過 ng-touched ng-untouched
值被改變 ng-dirty ng-pristine
值不符合驗證 ng-valid ng-invalid

可以用下面的範例來觀察input元素class名稱的改變(#spy代表將這個input存到一個叫做spy的變數內)

<input type="text" class="form-control" id="name"
  required
  &#91;(ngModel)&#93;="model.name" name="name"
  #spy>
<br />TODO: remove this:  {{spy.className}}


下面是類別改變的狀態表

Show and hide validation error messages

在做表單驗證時,時常會有如下圖這樣的需求

若要使用表單驗證,需要在input宣告ngModel(不論有沒有做雙向綁定),或者宣告formControlName或formControl,不然無法驗證。
下面是一個最簡單的required驗證宣告:

<input name="fullName" ngModel required>

如果要用客製化的訊息去告訴使用者那邊輸入錯誤的話,則可以將ngModel的值輸出成一個模版變數#name

<label for="name">Name</label>
<input type="text" class="form-control" id="name"
       required
       &#91;(ngModel)&#93;="model.name" name="name"
       #name="ngModel">

在上面我們看到#name="ngModel",這是因為ngModel這個directive的exportAs的值剛好就是ngModel。
這代表將input的ngModel存進一個叫做name的變數裡,下面就可以使用這個ngModel的模型顯示相關的錯誤訊息。

<div &#91;hidden&#93;="name.valid || name.pristine"
     class="alert alert-danger">
  Name is required
</div>

pristine的意思是未被改變過的,而name.valid則是檢查所有我們所設定的驗證器,回傳是否合法或者不合法。
因此[hidden]="name.valid || name.pristine"的意思就是如果尚未填到該選項或者填入的值是合法的,則不顯示錯誤訊息。

現成的forms Validator

在上面的例子中,我們在input中下了required來做必填欄位的驗證。
required是一個directive,我們可以在這邊看到這個directive的詳細使用說明。
在說明網頁中,有幾個是需要去看的,首先就是selector,代表我們要如何去使用這個驗證器。
以required為例,有三種選擇required的方法如下圖:

這個圖代表了required不能用在checkbox,要使用這個directive要在input上加上required以及ngModel(或者formControl, formControlName)屬性。

其他Angular有提供的驗證器如下

客製化模板驅動表格驗證器

除了上述的內建驗證器之外,我們也可以自己製作自製的驗證器directive

@Directive({
  selector: '[forbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
  @Input() forbiddenName: string;

  validate(control: AbstractControl): {[key: string]: any} {
    return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                              : null;
  }
}

在使用上的部份,就可以直接像下面這樣

<input id="name" name="name" class="form-control"
               required minlength="2" forbiddenName="bob"
               &#91;(ngModel)&#93;="hero.name" #name="ngModel" >
<div *ngIf="name.errors.forbiddenName">
            Name cannot be Bob.
          </div>

要注意的是,官網範例forbidden-name.directive.ts的selector名稱寫錯,不能使用appForbiddenName,要用forbiddenName,才能對的起來
這邊有ISSUE回報: Custom Validator example on angular.io directive selector issue.
詳情請見線上範例:live example

使用ngSubmit送出表單

在ngForm裡面,submit的button按下後並不會直接發生任何事,而是會觸發一個ngSubmit的事件,因此需要在form的地方去註冊ngSubmit事件

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">

在上面的程式碼裡,我們將這個表單ngForm存至heroForm這個變數裡,因此在submit的按鈕可以存取ngForm裡的值來判段這個表單是否已通過驗證

<button type="submit" class="btn btn-success" &#91;disabled&#93;="!heroForm.form.valid">Submit</button>

參考資料

[功能介紹-8] Pipes

Pipes有那些?

常用的Pipes有DatePipeUpperCasePipeLowerCasePipeCurrencyPipe,和PercentPipe。它們都可用於任何模板。

下面是angular內建所有的的Pipes說明:

如何使用Pipes

管道將數據作為輸入並將其轉換為所需的輸出。下面是使用DatePipe的範例。

import { Component } from '@angular/core';

@Component({
  selector: 'app-hero-birthday',
  template: `<p>The hero's birthday is {{ birthday | date }}</p>`
})
export class HeroBirthdayComponent {
  birthday = new Date(1988, 4, 15); // April 15, 1988
}

在Pipes裡使用參數

請見下面的範例

<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>
<p>The hero's birthday is {{ birthday | date:"shortDate" }} </p>
<p>The hero's birthday is {{ birthday | date:"fullDate" }} </p>

這樣會顯示結果如下:
MM/dd/yy:04/15/88
shortDate:04/15/1988
fullDate:Friday, April 15, 1988

串接多個管道

下面為一個例子

{{ birthday | date:'fullDate' | uppercase}}

結果會顯示
FRIDAY, APRIL 15, 1988

定義自己的Pipes

下面是一個自製Pipes的例子:

import { Pipe, PipeTransform } from '@angular/core';
/*
 * Raise the value exponentially
 * Takes an exponent argument that defaults to 1.
 * Usage:
 *   value | exponentialStrength:exponent
 * Example:
 *   {{ 2 | exponentialStrength:10 }}
 *   formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

在上面的例子中可以看到

  • 會以@Pipe({name:’XXXX’})來宣告這個class是一個pipe
  • pipe類別需implements PipeTransform介面並依照要input的值來選擇要實作的transform方法
  • transform有一個可選參數exponent,可讓使用者帶要帶入的參數進Pipes
  • Pipe的名字需要是一個合法的Javascript命名

下面是一個使用範例

import { Component } from '@angular/core';

@Component({
  selector: 'app-power-booster',
  template: `
    <h2>Power Booster</h2>
    <p>Super power boost: {{2 | exponentialStrength: 10}}</p>
  `
})
export class PowerBoosterComponent { }

顯示的結果如下:

在每次被綁定的值更動時,都會再跑一次Pipes的功能,一般來說,Pipe只會偵測值的變化才會執行pure Pipes。如對象是String、Number、Boolean、Symbol、Date、Array」Function、Object。但如果裡面是一個物件,則pure Pipes會忽略他的更動。
這是因為效能的考量,若為純粹物件的值的更動在偵測上較快,但是在物件上屬性的更改的偵測效能會較差。會建議改使用元件的方式去偵測改變。

但angular還是提供了impure pipes的方式可以偵測物件的改變,但使用上要小心不能因此而拖慢系統速度。
它看起來會是像這樣:

@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}

[功能介紹-7] Structural Directives

什麼是結構指令

結構指令負責HTML佈局。他們通常通過添加,刪除或操縱元素來塑造或重塑DOM的結構。結構指令很容易識別。通常會有星號(*)在指令屬性名稱前面。

以下是三種常件的結構指令的範例

<div *ngIf="hero" >{{hero.name}}</div>

<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<!--hero?.emotion指的是hero.emotion,?代表如果hero為空時不會因此引發null reference exception-->
<div &#91;ngSwitch&#93;="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    &#91;hero&#93;="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      &#91;hero&#93;="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'app-confused'" &#91;hero&#93;="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           &#91;hero&#93;="hero"></app-unknown-hero>
</div>

一個attribute directive改變了元素的外觀或行為。例如,內置的NgStyle指令同時改變幾個元素樣式。
而Structural Directives則是會自動將裡面的內容儲存成一個ng-template並且操縱它,這也是為什麼這些指令前面會有一個星號(*)。
其實這個星號會悄悄的讓這個directive成為structure的directive
例如

<div *ngIf="hero" >{{hero.name}}</div>

其實等同於

<ng-template &#91;ngIf&#93;="hero">
  <div>{{hero.name}}</div>
</ng-template>

我們可以觀察到

  • *號將該ngIf改為一個屬性綁定的元素,並綁定一個ng-template。*ngIf <ng-template> [ngIf]
  • 剩下的部分<div>,包括它的class屬性,移到了<ng-template>元素之下。

我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。

ng-template

從上面*所做的事情,我們可以知道,結構型指令是建立在ng-template之上的。
一個ng-template並不會一開始就顯示在畫面上,而是通過directive去操作裡面的dom並將要顯示的template添加在dom之中。

因此,*ngIf隱藏掉的物件,和我們使用css去show、hide在意義上是完全不同的。
因為它已經不在dom之上,是沒辦法被操作的。

不過由於若是網頁內的資料量大,angular有足夠的理由這樣做。
因為這可以避免過多的dom元素拖累網頁效能,若單純使用css去hide、show元素,所有的監聽器、物件依舊會在背景執行,這會讓效能變得不佳。
如果我們需要在show、hide物件的同時執行一些特殊的指令,可以用Lifecycle Hooks來撰寫此時要做的事情。

如何套用多個結構指令在元件裡

上面有提到,我們可以將許多屬性指令寫在同一個host element上,但同一個host element只能夠有一個結構指令。
所以在一般的狀態下,如果需要兩個標籤,我們會將HTML利用一些不會影響結構的標籤來做多層的Structural Directives控制

<div *ngIf="hero"><span *ngFor="hero of heroes">{{hero.name}} </span></div>

但有時候該狀況不允許任何多餘的標籤在裡面,例如下拉選單select。
若有一個區域選單select裡面的option內容要由*ngFor來產生,但是當city未選擇時,select又希望不要有任何下拉選單,
這時後我們在option上的確同時會需要放許多個結構指令,但ANGULAR不允許同一個標籤上放兩個結構指令。

如果我們多包一層span去包裡面的內容,會發現因為select內不允許span標籤,會造成讀不到option下拉選單,即便已經選擇了city

<div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select &#91;(ngModel)&#93;="hero">
  <span *ngFor="let h of heroes">
    <span *ngIf="showSad || h.emotion !== 'sad'">
      <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
    </span>
  </span>
</select>


這時候就可以改用<ng-container>

<select &#91;(ngModel)&#93;="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option &#91;ngValue&#93;="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>

就可以正常顯示了

自製一個結構Directive

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

/**
 * Add the template content to the DOM unless the condition is true.
 */
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) { }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

最重要的地方是在@Input那段,展示了如何去操作dom
使用的地方如下:

<p *appUnless="condition" class="unless a">
  (A) This paragraph is displayed because the condition is false.
</p>

完整範例請見:live example / download example

[功能介紹-6] Attribute Directives

Directive有分為結構型和屬性型的,這一篇要先介紹屬性型的Directive。
本篇的範例請見:Attribute Directive example / download example

Angular有三種directive:

  • 元件 – 包含template的directive。
  • 結構指令 – 通過添加和刪除DOM元素來更改DOM佈局。(如ngFor、ngIf)
  • 屬性指令 – 改變元素,組件或其他指令的外觀或行為。(如ngStyle)

建立一個簡單的屬性型directive

一個directive最少需要一個帶有@Directive宣告的檔案,告訴angular該如何識別這個directive。
這邊會用一個high light的directive範例來解說,當用戶將鼠標懸停在該元素上時,設置元素的背景色。你可以像這樣應用它:

<p appHighlight>Highlight me!</p>

而下面會一步步創立這一個directive。

首先,用cli創立一個新的directive

ng generate directive highlight

會看到cli自動建立了兩個新的檔案

產生的src/app/highlight.directive.ts檔案的內容如下:

import { Directive } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor() { }
}

當我們import了Directive後,就可以在檔案內使用@Directive來設定directive的CSS attribute selector
方括弧[]可以讓它成為一個屬性選擇器,在這邊為什麼會取為appHighlight而不是highlight,是為了避免與現有的html指令衝突,建議最好在所有的directive前面加上前綴字,以方便識別這是那邊來的屬性。如果沒有前綴字,很容易會發生難以識別的錯誤

接著看src/app/highlight.directive.ts的內容:

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}

這邊可以看到在建構子的地方會傳入ElementRef,並且使用el.nativeElement直接修改DOM元素的背景顏色。

若希望directive能夠接收使用者事件,可以加載HostListener,如下:

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) { }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }

顯示的結果如下圖:

當然,也可以通過標準的JavaScript來訪問DOM,並手動添加事件監聽器。這種方法至少有三個問題,是不被建議的:

  • 需正確的堅聽到各種平台的事件
  • 當要destroy這個directive時要手動移除事件
  • 直接操作DOM API不是好的方式

輸入參數至directive

首先要先import所需的Input

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

然後宣告所要輸入的變數

@Input() highlightColor: string;

那麼在使用directivet傳入參數的方式有下面幾種

<p appHighlight highlightColor="yellow">Highlighted in yellow</p>
<p appHighlight &#91;highlightColor&#93;="'orange'">Highlighted in orange</p>
<p &#91;appHighlight&#93;="'orange'">Highlight me!</p>

下面這個範例是使用上面自製的directive,讓使用者選擇highlight的顏色
template檔案內容如下:

<h1>My First Attribute Directive</h1>

<h4>Pick a highlight color</h4>
<div>
  <input type="radio" name="colors" (click)="color='lightgreen'">Green
  <input type="radio" name="colors" (click)="color='yellow'">Yellow
  <input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p &#91;appHighlight&#93;="color">Highlight me!</p>

component內容如下:

export class AppComponent {
  color: string;
}

最後結果如下圖:

[功能介紹-5] 動態載入元件

模版組件有的時後會需要能夠動態被載入,在這邊會講要如何利用ComponentFactoryResolver來動態加入組件

本篇的例子是需要動態載入廣告橫幅,因為廣告內容會由幾個不同的團隊來打造,要在同一個區塊循環播放不同的廣告,因此較難把不同的廣告放在同一個component,這時後就會需要用到動態載入的功能
完整範例請見: live example / download example

The anchor directive

在架構介紹的地方有介紹到,directive是一個沒有view的component,可以使用在html裡。
在這邊因為需要動態載入元件,會需要先建立一個directive,讓angular知道要把要動態載入的元件放在那邊。
ad.directive.ts這個檔案的內容如下:

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[ad-host]',//這個directive的名字
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }//在directive裡去取得View元件以方便做操作
}

首先我們將這個directive命名為[ad-host],然後注入ViewContainerRef,ViewContainerRef可以讓我們得知目前所在的HTML元素中包含的View內容,也可以透過它來改變View的結果(ex: 動態的產生Component、移除某個Component等等)。

Loading components

使用上面的directive的html碼如下

<div class="ad-banner">
   <h3>Advertisements</h3>
   <ng-template ad-host></ng-template>
</div>

這邊可以看到我們使用ng-template來應用剛剛所創建的directive,ng-template指令表示一個Angular模板,這個標籤的內容將包含template的一部分,然後可以與其他template一起組成來成為最終的組件模板。使用ng-template標籤只是定義一個template,但是我們還沒有使用它。

Resolving components

下面是src/app/ad-banner.component.ts的內容

export class AdBannerComponent implements AfterViewInit, OnDestroy {
  @Input() ads: AdItem[];//要輪播顯示的廣告
  currentAddIndex: number = -1;
  @ViewChild(AdDirective) adHost: AdDirective;//宣告剛剛建立的Directive,並藉由取得傳入的adHost
  subscription: any;
  interval: any;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  ngAfterViewInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }
  //最重要動態載入廣告的程式碼都在這個function裡
  loadComponent() {
    //決定要顯示那則廣告
    this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
    let adItem = this.ads[this.currentAddIndex];

    //透過ComponentFactoryResolver來解析component組件
    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
    //透過剛剛從AdDirective傳來的資訊取得要載入ad的template
    let viewContainerRef = this.adHost.viewContainerRef;
    //將template內容清掉
    viewContainerRef.clear();
    //動態建立template並顯示在畫面上
    let componentRef = viewContainerRef.createComponent(componentFactory);
    (componentRef.instance).data = adItem.data;
  }

  //通過這個方法,每三秒會動態載入不同的廣告元件
  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}

loadComponent是動態載入元件最關鍵的程式碼片段,程式碼裡都有下註解。

最終出來的成果如下圖

參考資料

[功能介紹-4] 組件間的溝通

通過輸入綁定數據從父項傳遞給子項

這是接受數據的子元件的內容hero-child.component.ts
子元件輸要被輸入的屬性有兩個,以@Input開頭來做宣告:

import { Component, Input } from '@angular/core';

import { Hero } from './hero';

@Component({
  selector: 'app-hero-child',
  template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
  `
})
export class HeroChildComponent {
  @Input() hero: Hero;
  @Input('master') masterName: string;
}

而這是父元件的內容,透過屬性綁定將值塞進子元件中

<app-hero-child *ngFor="let hero of heroes"
      &#91;hero&#93;="hero"
      &#91;master&#93;="master">

父元件的完整程式碼如下:

import { Component } from '@angular/core';

import { HEROES } from './hero';

@Component({
  selector: 'app-hero-parent',
  template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>
    <app-hero-child *ngFor="let hero of heroes"
      &#91;hero&#93;="hero"
      &#91;master&#93;="master">
    </app-hero-child>
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}

最後結果呈現:

攔截屬性的更改與設置

可將子元件的input宣告改為getter和setter,如下面範例

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  private _name = '';

  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }

  get name(): string { return this._name; }
}

這樣當父元件傳空字串來時,子元件會自動輸出<no name set>字樣

Intercept input property changes with ngOnChanges()

在子組件裡,除了上述使用getter及setter外,也可以利用ngOnChanges()來取得改變的變數並做出回應
ngOnChanges()會傳入所有被改變的值changes: {[propKey: string]: SimpleChange}
下面是一個使用範例
子元件內容

import { Component, Input, OnChanges, SimpleChange } from '@angular/core';

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];

  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}

父元件內容

import { Component } from '@angular/core';

@Component({
  selector: 'app-version-parent',
  template: `
    <h2>Source code version</h2>
    <button (click)="newMinor()">New minor version</button>
    <button (click)="newMajor()">New major version</button>
    <app-version-child &#91;major&#93;="major" &#91;minor&#93;="minor"></app-version-child>
  `
})
export class VersionParentComponent {
  major = 1;
  minor = 23;

  newMinor() {
    this.minor++;
  }

  newMajor() {
    this.major++;
    this.minor = 0;
  }
}

最後成果如下圖

Parent listens for child event

子元件的EventEmitter財產是一個輸出的屬性,通常用@Output來宣告,如下:

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  &#91;disabled&#93;="voted">Agree</button>
    <button (click)="vote(false)" &#91;disabled&#93;="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;

  vote(agreed: boolean) {
    this.onVoted.emit(agreed);
    this.voted = true;
  }
}

而父元件的內容如下:

import { Component }      from '@angular/core';

@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
      &#91;name&#93;="voter"
      (onVoted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];

  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

結果如圖:

Parent interacts with child via local variable

下面是範例,將子元件使用#來宣告為template變數,就可以在父元件內自由使用子元件變數:

<h3>Countdown to Liftoff (via local variable)</h3>
  <button (click)="timer.start()">Start</button>
  <button (click)="timer.stop()">Stop</button>
  <div class="seconds">{{timer.seconds}}</div>
  <app-countdown-timer #timer></app-countdown-timer>

Parent and children communicate via a service

服務內容:

import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs/Subject';

@Injectable()
export class MissionService {

  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();

  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();

  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }

  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }
}

父元件內容

import { Component }          from '@angular/core';

import { MissionService }     from './mission.service';

@Component({
  selector: 'app-mission-control',
  template: `
    <h2>Mission Control</h2>
    <button (click)="announce()">Announce mission</button>
    <app-astronaut *ngFor="let astronaut of astronauts"
      &#91;astronaut&#93;="astronaut">
    </app-astronaut>
    <h3>History</h3>
    <ul>
      <li *ngFor="let event of history">{{event}}</li>
    </ul>
  `,
  providers: [MissionService]
})
export class MissionControlComponent {
  astronauts = ['Lovell', 'Swigert', 'Haise'];
  history: string[] = [];
  missions = ['Fly to the moon!',
              'Fly to mars!',
              'Fly to Vegas!'];
  nextMission = 0;

  constructor(private missionService: MissionService) {
    missionService.missionConfirmed$.subscribe(
      astronaut => {
        this.history.push(`${astronaut} confirmed the mission`);
      });
  }

  announce() {
    let mission = this.missions[this.nextMission++];
    this.missionService.announceMission(mission);
    this.history.push(`Mission "${mission}" announced`);
    if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
  }
}

子元件內容

import { Component, Input, OnDestroy } from '@angular/core';

import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';

@Component({
  selector: 'app-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        &#91;disabled&#93;="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;

  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }

  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

結果如下圖:

更多本篇完整範例請見: live example / download example

[功能介紹-3] Hooks的生命週期

Lifecycle Hooks


一個組件有一個由Angular管理的生命週期。
Angular創建、產生元件,當元件的數據綁定屬性改變時做檢查並確認,並在元件從DOM中刪除它之前destroys掉該元件。
Angular提供lifecycle hooks,可以讓我們在各個階段加上我們要讓元件做的事情。
directive具有相同的lifecycle hooks。

如何使用Lifecycle Hooks

下面為一個使用範例

export class PeekABoo implements OnInit {
  constructor(private logger: LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this.logIt(`OnInit`); }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}

Lifecycle sequence

目的和時機
ngOnChanges() Angular設置數據綁定的輸入屬性。該方法接收SimpleChanges當前和以前的屬性值的對象。

會在ngOnInit()之前被呼叫

ngOnInit() 在Angular之後初始化指令/組件首先顯示數據綁定屬性並設置指令/組件的輸入屬性。
ngDoCheck() 檢測Angular無法或無法自行檢測到的更改並採取相應措施。
ngAfterContentInit() 在Angular將外部內容設置到template之後被呼叫。

 

ngAfterContentChecked() 在Angular檢查投影到組件中的內容之後被呼叫。
ngAfterViewInit() 初始化組件的template和sub-template之後被呼叫。
ngAfterViewChecked() 在Angular檢查組件的視圖和子視圖之後作出響應。
ngOnDestroy 在Angular破壞指令/組件之前進行清理。取消訂閱Observables和事件處理程序以避免內存洩漏。

範例練習

範例網址:https://angular.io/guide/lifecycle-hooks#peek-a-boo

[功能介紹-2] 資料繫結的模版語法

Template Syntax

在上一篇的Angular架構中有提到,透過模版語法,template可以與component做許多的溝通。

那這一篇就會介紹Angular內建的模版語法參考。

對於tempalte.html來說,所有的html標籤都可以使用,除了<script>以外,這是為了維護模版的安全性,去除template被攻擊的風險,因此在template中所有的<script>會被忽略,並跳出警告。

接下來我們來介紹所有Angular的模板語法

{{…}}

以下為一個範例

<h3>
  {{title}}
  <img src="{{heroImageUrl}}" style="height:30px">
</h3>

大括號之間值通常是組件屬性的名稱。Angular使用相應組件屬性的字符串值替換該名稱。
在上面的例子中,Angular會取元件裡title和heroImageUrl的屬性,並會取代{{title}}及{{heroImageUrl}},因此在頁面上會顯示一個大的應用程序標題,然後一個英雄影像的位置。

在{{…}}之間,我們也可以使用模板表達式去轉換要顯示的值。
例如在刮弧中做運算:

<!-- "The sum of 1 + 1 is 2" -->
<p>The sum of 1 + 1 is {{1 + 1}}</p>

或者也可以呼叫component的function getVal():

<!-- "The sum of 1 + 1 is not 4" -->
<p>The sum of 1 + 1 is not {{1 + 1 + getVal()}}</p>

大多的運算符都可以用在表達式裡面,除了一些會影響到component的值的運算符,如=、+=、-=之類。

有時候{{…}}裡面要綁定的數值也可以是在template定義的(使用#符號),請見下面的範例

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<input #heroInput> {{heroInput.value}}

「註」:如果在元件裡已經有變數名稱叫做hero,而template裡又有一個hero,在template會優先使用template內定義的變數。
在表達式中不能引用任何除了undefined外的全域變數,如window或document,也不能用consolo.log,只能使用元件內的屬性或方法,或者template裡上下文內的成員

在使用{{…}}時,有下面四個原則要注意:

  • No visible side effects:不應該改變任何元件內的值,在rendering整個表式示時應該是穩定的
  • Quick execution:表達式的運算應該要很快,因為它會在許多狀況下被呼叫,因此若是裡面含有許多複雜運算時,請考慮用快取以增加效能。
  • Simplicity:雖然可以在{{…}}裡面寫很複雜的運算但是不建議,最多在裡面使用!符號,不然還是建議將運算放到元件內去計算,以利閱讀及開發
  • Idempotent: idempotent的意思是如果相同的操作再執行第二遍第三遍,結果還是跟第一遍的結果一樣 (也就是說不管執行幾次,結果都跟只有執行一次一樣)。

(event)=”statement”

(event)=”statement”是Template statements。事件綁定是利用這樣的方式去做的,下面是一個範例:

<button (click)="deleteHero()">Delete hero</button>

與{{…}}不同的是,在這樣的語法中,是可以去改變元件的值的,並且被改變的值會透過單向綁定{{…}}顯示在畫面上。因此,(event)=”statement”的statement是能夠有支援=運算符的,但是+=、-=和++、–是不被允許的。

語句上下文也可以引用模板自己的上下文的屬性。在以下範例中,將模版$event對象,模板輸入變量(let hero)和模板引用變量(#heroForm)傳遞給組件的事件處理方法。
下面是一個範例

<button (click)="onSave($event)">Save</button>
<button *ngFor="let hero of heroes" (click)="deleteHero(hero)">{{hero.name}}</button>
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>

[target]=”statement”

以下是一個範例:

<!-- Bind button disabled state to `isUnchanged` property -->
<button &#91;disabled&#93;="isUnchanged">Save</button>

這樣的語法,能夠讓isUnchanged為true時,顯示這樣的畫面

<button disabled>Save</button>

綁定方法整理

資料方向 語法 類型
單向綁定
從資料源到view
{{expression}}
[target]="expression"
bind-target="expression"
Interpolation
Property
Attribute
Class
Style
單向綁定
從view的目標到資料源
(target)="statement"
on-target="statement"
Event
雙向綁定
[(target)]="expression"
bindon-target="expression"
Two-way

HTML attribute與DOM property

HTML attribute和DOM property(物件屬性)的區別對於理解Angular綁定是很重要的。一旦使用插值(`{{…}}`),就不是使用HTML attribute,而是在設置DOM property。

一些HTML attribute可以1:1的對應到一個DOM property,例如:id
一些HTML attribute沒有相應的DOM property,例如:colspan(無法使用插值)。
一些DOM property沒有相應的HTML attribute,例如:textContent
許多DOM property似乎對應到HTML attribute…但不是以你想像的方式!

HTML attribute的value指定初始值; DOM property的value屬性是當前值

例如,當瀏覽器執行下面HTML,它創建一個對應的DOM節點,其value屬性初始化為“Bob”。

<input type="text" value="Bob">

當用戶在輸入框中輸入“Sally”時,DOM property的value變成“Sally”。
但是,HTML attribute的value保持不變,如下所示:

input.getAttribute('value');
//取的的值還是返回“Bob”

disabled屬性是另一個特殊的例子。按鈕的disabled 屬性是false默認的,所以按鈕被啟用。當你添加disabled 屬性時,它的存在會初始化按鈕的disabled 屬性,true所以按鈕被禁用。添加和刪除disabled屬性禁用並啟用按鈕。屬性的值是不相關的,這就是為什麼你不能用下面的語法來將按鈕設為enable。

<button disabled="false">Still Disabled</button>

因此,HTML attribute和DOM property是不一樣的,即使它們具有相同的名稱。

綁定目標整理

類型 目標 例子
屬性 元素屬性
組件屬性
指令屬性
<img &#91;src&#93;="heroImageUrl">
<app-hero-detail &#91;hero&#93;="currentHero"></app-hero-detail>
<div &#91;ngClass&#93;="{'special': isSpecial}"></div>
事件 元素事件
組件事件
指令事件
<button (click)="onSave()">Save</button>
<app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail>
<div (myClick)="clicked=$event" clickable>click me</div>
雙向 事件和財產
<input &#91;(ngModel)&#93;="name">
屬性 屬性(例外)
<button &#91;attr.aria-label&#93;="help">help</button>
class 屬性
<div &#91;class.special&#93;="isSpecial">Special</div>
樣式 style 屬性
<button &#91;style.color&#93;="isSpecial ? 'red' : 'green'">

Property binding or interpolation?

屬性綁定與插值常常能達到相同的功效,如下面的範例:

<p><img src="{{heroImageUrl}}"> is the <i>interpolated</i> image.</p>
<p><img &#91;src&#93;="heroImageUrl"> is the <i>property bound</i> image.</p>

<p><span>"{{title}}" is the <i>interpolated</i> title.</span></p>
<p>"<span &#91;innerHTML&#93;="title"></span>" is the <i>property bound</i> title.</p>

一般而言,為了易讀性,會使用插值({{…}})。但當沒有要綁定的元素屬性時,必須使用屬性綁定。例如:

<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>

會得到這個錯誤
Template parse errors:
Can't bind to 'colspan' since it isn't a known native property

這是因為插值只能設定properties而不能設定attributes。
這時可以改成

<table border=1>
  <!--  expression calculates colspan=2 -->
  <tr><td &#91;attr.colspan&#93;="1 + 1">One-Two</td></tr>

  <!-- ERROR: There is no `colspan` property to set!
    <tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
  -->

  <tr><td>Five</td><td>Six</td></tr>
</table>

則可以正常顯示如下圖

Built-in structural directives

常見的結構指令如下:

  • NgIf – 有條件地從DOM中添加或刪除一個元素,要注意,這和css的show、hide不一樣,當元素被dom移除時,是沒有辦法去操作DOM元素裡的物件的。
    <app-hero-detail *ngIf="isActive"></app-hero-detail>
  • NgSwitch – 一組在不同視圖之間切換的指令
    <div &#91;ngSwitch&#93;="currentHero.emotion">
      <app-happy-hero    *ngSwitchCase="'happy'"    &#91;hero&#93;="currentHero"></app-happy-hero>
      <app-sad-hero      *ngSwitchCase="'sad'"      &#91;hero&#93;="currentHero"></app-sad-hero>
      <app-confused-hero *ngSwitchCase="'confused'" &#91;hero&#93;="currentHero"></app-confused-hero>
      <app-unknown-hero  *ngSwitchDefault           &#91;hero&#93;="currentHero"></app-unknown-hero>
    </div>

  • NgForOf – 為列表中的每個項目重複一個模板
    <app-hero-detail *ngFor="let hero of heroes" &#91;hero&#93;="hero"></app-hero-detail>

Template reference variables ( #var )

在Angular也可以使用#開頭來將使用者在網頁上input輸入的值轉為一個變數,如下:

<input #phone placeholder="phone number">

<!-- lots of other elements -->

<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>

這個功能在做表單驗證時非常的方便

<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
  <div class="form-group">
    <label for="name">Name
      <input class="form-control" name="name" required &#91;(ngModel)&#93;="hero.name">
    </label>
  </div>
  <button type="submit" &#91;disabled&#93;="!heroForm.form.valid">Submit</button>
</form>
<div &#91;hidden&#93;="!heroForm.form.valid">
  {{submitMessage}}
</div>

允許外部元件讀取元件內的屬性

要讓元件內的屬性能夠給其他元件使用,或者讀取其他元件的屬性,可以在component.ts內宣告

@Input()  hero: Hero;
@Output() deleteRequest = new EventEmitter<hero>();

或者這樣也可以

@Component({
  inputs: ['hero'],
  outputs: ['deleteRequest'],
})

輸入屬性通常接收數據值。 輸出屬性會發送事件,如EventEmitter。
下面的圖顯示元件屬性的輸入和輸出的範例。

safe navigation operator ( ?. )

為了防止出現null reference exception,我們可以使用?.,當值為空值時,會直接傳回空白,可以避免產生不必要的exception
以下為範例

The current hero's name is {{currentHero?.name}}

參考資料

完整綁定範例資料請見此:線上範例

[功能介紹-1] Angular架構

Angular是一個框架,用來編寫html的應用程式,可以用javascript或typescript去編寫。

下圖是官網上所繪製的Angular架構圖:

首先先看最中間那一塊,是由template、metadata、component所構成的,這三個是一個component必備的元素。
所謂的元件可以看之前創建hero那篇文章,我們可以用下面指令創一個元件

ng generate component selectSystem

創完一個元件後,可以看見下面這些檔案

Templates

裡面的component.html檔就是template,它看起來像是一個html的檔案,可以在裡面用資料綁定與事件綁定與controller裡的物件做繫結。
下面是一個template的範例:

<h2>Hero List</h2>

<p><i>Pick a hero from the list</i></p>
<ul>
  <li *ngFor="let hero of heroes" (click)="selectHero(hero)">
    {{hero.name}}
  </li>
</ul>

<app-hero-detail *ngIf="selectedHero" &#91;hero&#93;="selectedHero"></app-hero-detail>

可以注意到上面有些地方與一般的html不相同,例如像是*ngFor、{{hero.name}}、(click)="selectHero(hero)"等…
這就是架構圖畫面上用來連繫Component以及Template的兩個箭頭,property binding以及event binding。

例如click事件繫結是(click)=’functionName()’,物件繫結可以用{{data}})。
透過這樣的繫結可以讓template將使用者操作的事件傳給component,component也可以將資料的更動即時的反饋到template所顯示的資料上。

下圖是非常清楚的binding類型列圖

<li>{{hero.name}}</li>
<app-hero-detail &#91;hero&#93;="selectedHero"></app-hero-detail>
<li (click)="selectHero(hero)"></li>

其中{{hero.name}}為值繫結,可以綁定component裡的值。
[hero]="selectedHero"為property binding,可以將某個元件裡的變數塞進一個HTML元素的屬性裡。。
(click)="selectHero(hero)" event binding可以呼叫component裡的function

<input &#91;(ngModel)&#93;="hero.name">

這個則是雙向數據綁定,在雙向綁定中,與屬性綁定一樣,數據屬性值將從組件輸入到輸入框中。用戶的更改也會返回到組件,將屬性重置為最新值,就像事件綁定一樣。

數據綁定在模板及其組件之間的通信中起著重要的作用。

數據綁定對於父組件和子組件之間的通信也很重要。

Component

而component.ts檔則是component內容,裡面會有一些屬性或方法來供template呼叫,下面是一個Component的簡單範例:

export class HeroListComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(private service: HeroService) { }

  ngOnInit() {
    this.heroes = this.service.getHeroes();
  }

  selectHero(hero: Hero) { this.selectedHero = hero; }
}

當用戶在應用程序中切換畫面時,Angular會創建,更新和銷毀組件。你的應用程序可以通過可選參加這個生命週期的每個時刻動作的lifecycle hooks,像ngOnInit()。

Metadata

metadata則是在component.ts裡由@Component開頭的區塊來宣告,裡面會定義這個Component在要如何在別的元件的template裡被引用,templateUrl則是定義自己這個元件要顯示的html模版位置及styleUrls是css檔案位置。
例如下面這個metadata宣告
@Component({
selector: ‘app-heroes’,
templateUrl: ‘./heroes.component.html’,
styleUrls: [‘./heroes.component.css’]
})
就可以用下面的方式來顯示這個元件

<app-heroes></app-heroes>

Modules

接著來介紹左上方的區塊

Angular應用程序是模組化的,Angular稱自己為NgModules的模組化系統。NgModules是一個很大的議題,在後面會有另一篇文章專門介紹ngModules。
一個小的application至少會有一個模組,稱為root module。雖然有些小的專案可能就只有一個模組,但大多大的專案都會有多個模組,稱為feature modules,一個模組內會是相同工作範疇的一組元件,它們有許多工作流程或功能上緊密相關,彼此協同運作。
當我們執行了這樣的指令來創建一個新的專案時

ng new my-app

可以看到src/app資料夾內有app.module.ts這個檔案,這個檔案就是angular預設的根模組。
其內容如下:
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
@NgModule({
imports: [ BrowserModule ],
providers: [ Logger ],
declarations: [ AppComponent ],
exports: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
一個NgModule,無論是root module還是feature modules,該class裡一定會有@ngModule的宣告區域,在angular裡會有許多@開頭的宣告,這樣的宣告稱為decorator,可以在元件或模組裡設定許多metadata。關於ngModule更多說明可以看@NgModule的說明關於decorators更多的說明則可以看這邊,這兩項之後都會有專門的文章來介紹。

NgModule是一個用來描述這個模組裡有那些component的metadata,是一個decorator function。
在ngModule裡最重要的屬性有下面這五點:

  • declarations:屬於這個模組的成員。Angular有三種成員:components, directives,以及pipes。
  • exports:要將declarations宣告的那一些成員公開。讓其他模組引用此模組時,可以存取該成員的public function。
  • imports:需引用的模組,所有在這個模組內的元件要引用的模組都要在此宣告
  • providers:要引用的Service需在此宣告
  • bootstrap:這個屬性只有根模組需要設定,在此設定在一開始要顯示的application view

註:上面的範例中,因為有設定bootstrap代表其為一個根組件,而根組件是不需要exports出去的,因為不會有任何其他modules需要用到它。

而要啟動整個應用程序可以在main.ts中加上這段
import { enableProdMode } from ‘@angular/core’;
import { platformBrowserDynamic } from ‘@angular/platform-browser-dynamic’;

import { AppModule } from ‘./app/app.module’;
import { environment } from ‘./environments/environment’;

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);//用這個指令來啟動root modules

在js裡我們透過export一個class來供其他modules使用。並在其他js檔案利用import來將這個class引入。

export class AppModule { }
import { NgModule }     from '@angular/core';
import { AppComponent } from './app.component';

NgModules與JavaScript modules是完全不同的兩樣東西,我們可以同時透過ngModule與JS Modules來交互使用以達成我們所需要達成的目的。

詳細了解Web上的JavaScript模組系統

Angular libraries


angular內建了許多可供應用的模組,我們稱為library module。
所有的library module都以@angular為前綴開頭,可以使用npm來安裝管理它們。
例如要使用Component功能要引用@angular/core的Component

import { Component } from '@angular/core';

或是使用BrowserModule

import { BrowserModule } from '@angular/platform-browser';

Directives


在架構圖中的template右邊,可以看到有一個directive指向template。directive是透過Angular使用內建或自訂directive用來自己定義html元素,並簡化dom操作的功能。可以讓template裡去使用。
一個directive會由@Directive來宣告。其實directive與component的本質是相同的,只是component是一個有template的directive,而directive沒有。我們可以視component為directive的擴展,擴展了template的功能。不過因為component在angular是非常重要的,和directive有不同的意義,因此會將component以及directive在架構上分開來。

directives分為架構型的和屬性型的:
下面是兩個內建架構型的directive範例

<li *ngFor="let hero of heroes"></li>
<app-hero-detail *ngIf="selectedHero"></app-hero-detail>

*ngFor 會讓angular重覆寫許多的 <li>將heroes裡的資料跑過一圈
*ngIf 則會讓angular只有在selectedHero的值為true時才會顯示app-hero-detail元件
而屬性型的directive則例如像是做雙向繫結的ngModel,下面的片段程式會將hero.name的值塞入input的value屬性內,並且監聽使用者修改input的值的事件將修改傳回至hero.name

<input &#91;(ngModel)&#93;="hero.name">

Services


架構圖的左下區是很多的Service注入至Component裡。幾乎所有功能都可以是service,但是它應該是目的明確且狹義的功能。
例如:記錄服務、數據服務、消息服務、稅計算器、應用程序配置等…

以下是一個範例,在這個例子中可以看到我們可以利用getHeroes()和service取得Hero列表,而Service則負責與Backend溝通由API取得資料並且回傳給component

export class HeroService {
  private heroes: Hero[] = [];

  constructor(
    private backend: BackendService,
    private logger: Logger) { }

  getHeroes() {
    this.backend.getAll(Hero).then( (heroes: Hero[]) => {
      this.logger.log(`Fetched ${heroes.length} heroes.`);
      this.heroes.push(...heroes); // fill cache
    });
    return this.heroes;
  }
}

Dependency injection

依賴注入是一種設計模式,可參考這篇文章了解:理解 Dependency Injection 實作原理
在Angular中,大多數依賴是服務,Angular的元件使用依賴注入為自己提供他們需要的服務,對於元件來說,服務必須是完全依賴的,component會在元件內使用service所提供的方法來取得自己所需的資料。

HeroService依賴注入的過程看起來像這樣:

injector會有一個所有Service的集合。如果所需要的Service不在這個集合中,那麼injector將創建一個Service並加進Service集合裡。當component所需要的Service都已經取得後,只要在constructor設定該服務在此元件內的名稱,就可以在元件裡自由的使用服務了。

下面是一個依賴注入的範例

constructor(private service: HeroService) { }

例如上面的程式碼就會讓該component裡的程式碼可以使用service.getHeroes()來取得英雄列表。

在上圖中,上面要被injector選擇的那些service是怎麼來的呢?我們可在根模組或者自己所在的模組裡去提供所有需用到的服務,這樣在這個模組內的所有元件都可以使用這個服務,如下:
src / app / app.module.ts

providers: [
  BackendService,
  HeroService,
  Logger
],

或者也可以在該元件的metadata裡用providers來設定這個元件要使用這個服務,如下:

@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

參考資料