響應式表單提供了一種模型驅動的方式來處理表單輸入,其中的值會隨時間而變化。本文會向你展示如何建立和更新基本的表單控制元件,接下來還會在一個表單組中使用多個控制元件,驗證表單的值,以及建立動態表單,也就是在執行期新增或移除控制元件。

form 表單

這裡的 require 建儀是寫在 formGroup 裡面去作理管,才可方便的在 js 中看到何為必填值。

<form [formGroup]="subscribeForm" (ngSubmit)="submit()">
    <mat-form-field class="w-full">
        <input matInput required type="email" formControlName="email" />
        <mat-error> Please enter a valid email address </mat-error>
    </mat-form-field>
    <button type="submit">submit</button>
</form>

invalid function

透過下列這段 checked form 是否有尚未寫好的值,或是沒有通過的 validations。

this.formName.invalid;

disable enable

可把欄位 disable 和 enable,但是特別注意 disable() 的值,如果直接用 this.form.value 會拿不到值

this.formName.controls["control"].disable();
this.formName.controls["control"].enable();

如果想要拿到 disable() 的值,需要用 getRawValue() 來取得到已被 disable() 的值

this.formName.getRawValue();

FormGroup

How do I restrict an input to only accept numbers?

<input type="number" ng-model="myText" name="inputName">

How do I restrict an input to only accept numbers?

Nested FormGroup

form 的表單很大的情況,希望可以有結構化的去管理資料,可以 nest formGroup 的方式,方便一目了然的去抓到資料

this.contactForm = this.formBuilder.group({
    firstname: ["", [Validators.required, Validators.minLength(10)]],
    lastname: ["", [Validators.required, Validators.maxLength(15), Validators.pattern("^[a-zA-Z]+$")]],
    country: ["", [Validators.required]],
    address: this.formBuilder.group({
        city: ["", [Validators.required]],
				...
    }),
});
<div formGroupName="address">
    <div class="form-group">
        <label for="city">City</label>
        <input type="text" class="form-control" name="city" formControlName="city" />
    </div>
		...
</div>

Form Array

先前我對 FomrArray 一直有個誤解,就是以為在 Angular 的 Reactive Form 中,若表單裡的資料是 array 就要使用它;誰叫拿 angular 跟 form 、 array 三個關鍵字餵狗就會跑出它而事實上只要這樣寫就行了:

constructor(
  private fb: FormBuilder
) { }

this.form = this.fb.group({
  items: [[]]
})

若把 form.value console.log 出來就是:

// { items: [] }

那 FormArray 的真正用途是什麼?主要是讓我們能「陣列式」管理表單的控制項與資料,例如以下的通訊錄表單:

constructor(
  private fb: FormBuilder
) { }

this.form = this.fb.group({
  title: ['My Contacts', Validators.required],
  contacts: this.fb.array([
    this.fb.group({
        name: 'Jane Doe',
        phone: '12345678'
      })
    ])
})

每位聯絡人姓名與電話為一組在 contacts 陣列裡的物件,form.value 的資料結構會是:

{
  title: 'My Contacts',
  contacts: [{
    name: 'Jane Doe',
    phone: '12345678'
  }]
}

要在 html 中 render,用 formArrayName 連結 form 裡的 formArray 屬性名稱 "contacts",而 contacts 控制項作為 formArray ,其 controls 屬性為可供遍歷的 formGroup 陣列:

<form [formGroup]="form">
  <div
    formArrayName="contacts"
    *ngFor="let contact of form.get('contacts').controls; let i = index"
  >
    <ng-container [formGroup]="contact">
      <input
        formControlName="name"
        type="text"
      />
      ...
    </ng-container>
  </div>
</form>

注意 contacts 控制項並不是 JS array,Angular 定義的 FormArray 沒有像 splice() 等原生 JS 方法可用。

FormArray 操作

使用 push()

(<FormArray>this.form.get('contacts')).push(
  this.fb.group({
    name: 'John Doe',
    phone: '3345678'
  })
);

使用 removeAt(idx)

(<FormArray>this.form.get('contacts')).removeAt(idx);

patchValue

一般 form 裡面的資料可以用 patchValue()

this.form.patchValue({
  title: 'Another Contacts'
});

對於 FormArray 資料另外用 setControl() 去覆蓋:

this.form.setControl(
  'contacts',
  this.fb.array([
    this.fb.group({
      name: 'John Doe',
      phone: '3345678'
    })
  ])
);

Clear FormArray

要清空 FormArray 的元素,讓它長度為 0 時,用 removeAt() 去遞迴,寫成 function:

clearFormArray = (formArray: FormArray) => {
  while (formArray.length !== 0) {
    formArray.removeAt(0)
  }
}

另一個比較暴力的做法:

clearFormArray = (formArray: FormArray) => {
  formArray = this.formBuilder.array([]);
}

副作用是若有對 formArray.valueChanges 的訂閱,將會丟失 reference 而出錯,不太推薦。

Validations

下列是 angular 在 formGroup 中提供的 validations 的方式,當然也可以自己去寫 customer 的 validation function

Angular

class Validators {
  static min(min: number): ValidatorFn
  static max(max: number): ValidatorFn
  static required(control: AbstractControl): ValidationErrors | null
  static requiredTrue(control: AbstractControl): ValidationErrors | null
  static email(control: AbstractControl): ValidationErrors | null
  static minLength(minLength: number): ValidatorFn
  static maxLength(maxLength: number): ValidatorFn
  static pattern(pattern: string | RegExp): ValidatorFn
  static nullValidator(control: AbstractControl): ValidationErrors | null
  static compose(validators: ValidatorFn[]): ValidatorFn | null
  static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}

updateValidations

setErrors 如果是 null 的情況下,會把原本的 required 的設定也都拿掉,所以要特別小心

this.formName.get("formControlName").setValidators([Validators.required]);
//setting validations
this.formName.get("formControlName").setErrors({ required: true });
this.formName.get("formControlName").setErrors({ required: false }); // 會有 error 的style 不會有 msg
this.formName.get("formControlName").setErrors(null); // 清除 errors 的方法
//error message
this.myForm.controls["controlName"].clearValidators();
//clear valiations
this.formName.updateValueAndValidity();
//update validation
this.formName.controls.reset();
// 把 form 表中的資料回復到最一開始的樣子

NOTE 如果是在另一個 component 中要去 control 另一個 form 給它 setErrors 的話可以加上面這一段,這樣子才可以正常的設定 error

this.formName.markAsTouched({ onlySelf: true });

Angular reactive forms set and clear validators

https://www.tektutorialshub.com/angular/how-to-add-validators-dynamically-using-setvalidators-in-angular/

hasError

在操作 js 的時候,需要有一個特定的 error style 可以用 setErrors 來設定 booleantemplate 來使用

this.productsForm[index].controls["unit_price"].setErrors({ bottomPrice: true });
<mat-error *ngIf="productsForm[index].get('unit_price').hasError('bottomPrice')">
    <span>Please enter bottomPrice.</span>
</mat-error>

清除不需要 error style 可以把此方法,我原本設用的 error 移除

this.addProductForm.controls["part_no"].setErrors(null);
note;
const pattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}/;
// REF: https://www.w3resource.com/javascript/form/email-validation.php
const pattern = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;

Custom & Async Validators

Custom Validators

Custom Async Validators

import { Injectable } from '@angular/core';
import {AbstractControl, FormControl, FormGroup, ValidationErrors} from '@angular/forms';
import {Observable, of} from 'rxjs';
import {delay, map} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ZipcodeService {
  private validZipcodes = ['00001', '00002', '00003', '00004'];

  fakeHttp(value: string) {
    return of(this.validZipcodes.includes(value)).pipe(delay(5000));
  }
}
import { ZipcodeService } from "./zipcode.service";
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from "@angular/forms";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";

export class ZipcodeValidator {
    static createValidator(zipcodeService: ZipcodeService): AsyncValidatorFn {
        return (control: AbstractControl): Observable<ValidationErrors> => {
            return zipcodeService.fakeHttp(control.value).pipe(map((result: boolean) => (result ? null : { invalidAsync: true })));
        };
    }
}
import {Component, OnInit} from '@angular/core';
import { FormControl, FormGroup, Validators} from '@angular/forms';
import {ZipcodeService} from './zipcode.service';
import {ZipcodeValidator} from './zipcode-validator';

@Component({
  selector: 'app-async-validator-demo',
  templateUrl: './async-validator-demo.component.html',
  styleUrls: ['./async-validator-demo.component.scss']
})
export class AsyncValidatorDemoComponent implements OnInit {
  address: FormGroup;
  zipcodeSyncValidators = [
    Validators.required,
    Validators.pattern('\\d{5}')
  ];

  constructor(private zipcodeService: ZipcodeService) {}

  ngOnInit(): void {
    this.address = new FormGroup({
      zipcode: new FormControl('',
       this.zipcodeSyncValidators,
       ZipcodeValidator.createValidator(this.zipcodeService))
    });
  }
}

Angular: Custom Async Validators

cross reative form

Cross Field Validation Using Angular Reactive Forms - Offering Solutions Software

自行觸發驗證

有時候的情境會需要特別用 js 來觸發驗證,就可以用此方式來確認。

this.form.controls["part_no"].markAllAsTouched();

Reset Reactive Form

有些情況下,submit form 之後,要在清除 form 的資料,可用下列的方式去作 clear 的動作。

<form [formGroup]="addProductForm" (ngSubmit)="addProduct()" #formDirective="ngForm">...</form>
export class CorporateQuotationPageComponent implements OnInit {
   @ViewChild('formDirective') formDirective;
	 ...
   resetForm() {
  		this.formDirective.resetForm(); //重新的把值清空
   }
}

reset() vs resetForm() ??

這兩個有什麼差別呢?實際上在專案遇到的情況。如果使用 form 的 submit 是用 ngSubmit 的話,會需要用 resetForm 來清掉原本的值,不然 require 的錯誤會跑出來。

如果你用 click button 的方式,可以直接用 reset 就可以,require 的錯誤不會跑出來

ValueChange

使用 setValue 和 patchValue 其實是一樣的,主要都是會去觸發,updateValueAndValidity 這一個 function

this.formName.controls["control"].setValue(data);
this.formName.controls["control"].patchValue(data);

不管是 formGroup 和 formControl 都可以用 subscribe 去作訂閱

this.formName.valueChanges.subscribe(()=> {...})
this.formName.controls['control'].valueChanges.subscribe(()=> {...})

如果是一個 onlySelf 的 setValue 的話,那就不會改變 _parent 的 value。

// updateValueAndValidity 內容的其中一段
if (this._parent && !opts.onlySelf) {
    this._parent._updateTouched(opts);
}

目前看到官方文件是寫 onlySelf 預設值為 false ,但是有些文章是 true ,這個就有待確認了,可能改過版了。

參考 source code

另外一個常用到的就是 emitEvent ,如果希望在 setValue 的時候不要去觸發 valueChange 的話,就可以用

this.formName.controls["control"].patchValue(data, { emitEvent: false });

比較一下這兩個 option 的用法

onlySelf

  1. ture 只更新此 control ,不會更新到 parent 的值
  2. (預設) false 會影響 ancestors( parent 的資料 )

emitEvent

  1. true 會觸發 statusChanges 跟 valueChanges observable,且含最新的 status 跟 value
  2. (預設) false 不會 emit event

value change 參考文章

Problem

如果你看到這個 error msg 的話
Angular reactive Form error: Must supply a value for form control with name

表示你在更新 form 表單的資料,是用 setValue ,但是沒有完整的更新所有的值,如果你只是要更新部分的資料的話,可以用 patchValue()

https://stackoverflow.com/questions/51047540/angular-reactive-form-error-must-supply-a-value-for-form-control-with-name