iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Modern Web

Angular 深入淺出三十天:表單與測試系列 第 29

Angular 深入淺出三十天:表單與測試 Day29 - ControlContainer

Day29

昨天跟大家分享了自訂表單元件的作法,但昨天的作法只適用於一個欄位、一個 FormControl

雖然 FormControl 裡是可以設 {} 的值,但如果我們真的想要的是一個可以直接用 [formGroup][formArray] 所使用的元件呢?

沒問題,只要你想要, Angular 都給你

實作開始

大家還記得之前我們做了個「被保人表單」吧?

一開始只有「姓名」、「性別」跟「年齡」這三個欄位,後來我們加了「聯絡資訊」的欄位,這次我們再幫它加個「聯絡地址」的欄位吧!

一般來說,聯絡地址的欄位通常會分成「縣市」、「鄉鎮市區」、「郵遞區號」與「地址」,而「縣市」、「鄉鎮市區」與「郵遞區號」之間會有一些連動邏輯,縣市」與「鄉鎮市區」這兩個欄位也通常會是下拉選單,其他的則是一般的 input 欄位。

首先一樣先把 HTML 準備好,像這樣:

<p><label>聯絡地址:</label></p>
<p>
  <select>
    <option value="">請選擇縣市</option>
  </select>
  <select>
    <option value="">請選擇鄉鎮市區</option>
  </select>
</p>
<p>
  <input type="text" style="width: 4rem" placeholder="郵遞區號">
  <input type="text" place="請輸入地址">
</p>

畫面:

Template View

樣式用 inline 的方式設定是方便教學,小朋友們要盡量少用噢!

接著,在 .ts 裡加入地址的相關欄位的 FormGroupFormControl

const addressInfoFormGroup = this.formBuilder.group({
  city: '',
  district: '',
  zip: '',
  address: ''
});

return this.formBuilder.group({
  name: [
    '',
    [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
  ],
  gender: ['', Validators.required],
  age: ['', Validators.required],
  contactInfoType: contactInfoTypeControl,
  contactInfo: contactInfoControl,
  addressInfo: addressInfoFormGroup
});

然後再將其綁與畫面上元素綁定,像這樣:

<ng-container formGroupName="addressInfo">
  <p><label>聯絡地址:</label></p>
  <p>
    <select formControlName="city">
      <option value="">請選擇縣市</option>
    </select>
    <select formControlName="district">
      <option value="">請選擇鄉鎮市區</option>
    </select>
  </p>
  <p>
    <input type="text" style="width: 4rem" placeholder="郵遞區號" formControlName="zip">
    <input type="text" placeholder="請輸入地址" formControlName="address">
  </p>
</ng-container>

連動邏輯的實作就交給大家練習囉,我們今天沒有要著重於此部分的處理。

至此,我們就完成了第一步的準備工作。

ControlContainer

接下來,我們就要將聯絡地址這塊拆成一個獨立的 Component ─ AddressInfoComponent

首先,先將 HTML 搬過去並稍微調整一下:

<ng-container [formGroup]="formGroup">
  <p>
    <select formControlName="city">
      <option value="">請選擇縣市</option>
    </select>
    <select formControlName="district">
      <option value="">請選擇鄉鎮市區</option>
    </select>
  </p>
  <p>
    <input type="text" style="width: 4rem" placeholder="郵遞區號" formControlName="zip">
    <input type="text" placeholder="請輸入地址" formControlName="address">
  </p>
</ng-container>

接著在 AddressInfoComponent 裡注入 ControlContainer

export class AddressInfoComponent {

  constructor(private controlContainer: ControlContainer) { }

}

然後加上:

get formGroup(): FormGroup {
  return this.controlContainer.control as FormGroup;
}

再回到被保人表單裡,把原本的聯絡地址區塊改成:

<p><label>聯絡地址:</label></p>
<app-address-info formGroupName="addressInfo"></app-address-info>

至此就大功告成了!是不是超簡單的?!

不過之所以這麼簡單是因為這是 Reactive Forms 的方式,今天的 ControlContainer 不像昨天的 ControlValueAccessor 可以做一次之後,兩種方式都可以使用。

如果今天這個元件是要讓 Template Driven Forms 使用的話,首先要先將 Template 原本用 Reactive Forms 的綁定方式改成使用 Template Driven Forms 的綁定方式,像是這樣:

<ng-container ngModelGroup="addressInfo">
  <p>
    <select name="zip" ngModel>
      <option value="">請選擇縣市</option>
    </select>
    <select name="district" >
      <option value="">請選擇鄉鎮市區</option>
    </select>
  </p>
  <p>
    <input type="text" style="width: 4rem" placeholder="郵遞區號" name="zip" ngModel>
    <input type="text" placeholder="請輸入地址" name="address" ngModel>
  </p>
</ng-container>

然後也不用在 AddressInfoComponent 裡注入 ControlContainer ,而是改在 AddressInfoComponentMetaDataviewProviders 裡新增以下設定:

@Component({
  selector: 'app-address-info',
  templateUrl: './address-info.component.html',
  styleUrls: ['./address-info.component.scss'],
  viewProviders:[
    {
      provide: ControlContainer, 
      useExisting: NgForm
    }
  ]
})
export class AddressInfoComponent {

這樣就能直接用 <app-address-info></app-address-info> 的方式使用這個元件了。

大家覺得,是 Reactive Forms 的方式好用,還是 Template Driven Forms 的方式好用呢?

本日小結

今天的重點主要是讓大家知道要怎麼使用 ControlContainer 這個類別來包裝我們的元件,以達到提昇重用性維護性的目的。

雖然麻煩的是,它沒辦法像昨天分享的 ControlValueAccessor 一樣,做好了之後可以適用於 Template Driven FormsReactive Forms ,但好在它的用法其實頗為簡單,主要的差異就只有在 Template Driven Forms 需要靠 viewProvider ,而 Reactive Froms 只要注入就行。

關於 viewProvider 與 provider 的差異,我推薦大家可以去看 Kevin (台灣 Angular GDE)的 [Angular] viewProviders V.S. providers ,我覺得寫得非常的清楚。

此外,如果覺得我分享不好,也可以參考 Kevin 的 [Angular] ControlContainer 的應用

今天的程式碼會放在 Github - Branch: day29 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!

如果有任何的問題或是回饋,還請麻煩留言給我讓我知道!


上一篇
Angular 深入淺出三十天:表單與測試 Day28 - 自訂表單元件
下一篇
Angular 深入淺出三十天:表單與測試 Day30 - 表單原理
系列文
Angular 深入淺出三十天:表單與測試30

尚未有邦友留言

立即登入留言