--- title: Reactive forms date: 2022-05-27 00:23:21 tags: [angular] categories: FrontEnd --- > **響應式表單**提供了一種模型驅動的方式來處理表單輸入,其中的值會隨時間而變化。本文會向你展示如何建立和更新基本的表單控制元件,接下來還會在一個表單組中使用多個控制元件,驗證表單的值,以及建立動態表單,也就是在執行期新增或移除控制元件。 ## form 表單 這裡的 require 建儀是寫在 formGroup 裡面去作理管,才可方便的在 js 中看到何為必填值。 ```html <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。 ```javascript this.formName.invalid; ``` ### disable enable 可把欄位 disable 和 enable,但是特別注意 `disable()` 的值,如果直接用 `this.form.value` 會拿不到值 ```javascript this.formName.controls["control"].disable(); this.formName.controls["control"].enable(); ``` 如果想要拿到 `disable()` 的值,需要用 `getRawValue()` 來取得到已被 `disable()` 的值 ```javascript this.formName.getRawValue(); ``` ## [FormGroup](https://www.tektutorialshub.com/angular/angular-formbuilder-in-reactive-forms/) How do I restrict an input to only accept numbers? ```jsx <input type="number" ng-model="myText" name="inputName"> ``` [How do I restrict an input to only accept numbers?](https://stackoverflow.com/questions/14615236/how-do-i-restrict-an-input-to-only-accept-numbers) ### Nested FormGroup form 的表單很大的情況,希望可以有結構化的去管理資料,可以 `nest formGroup` 的方式,方便一目了然的去抓到資料 ```typescript 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]], ... }), }); ``` ```html <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](https://yuugou727.github.io/blog/2019/06/30/angular-form-array/) 先前我對 FomrArray 一直有個誤解,就是以為在 Angular 的 Reactive Form 中,若表單裡的資料是 array 就要使用它;~~誰叫拿 angular 跟 form 、 array 三個關鍵字餵狗就會跑出它~~而事實上只要這樣寫就行了: ```typescript constructor( private fb: FormBuilder ) { } this.form = this.fb.group({ items: [[]] }) ``` 若把 `form.value` console.log 出來就是: ```javascript // { items: [] } ``` 那 FormArray 的真正用途是什麼?主要是讓我們能「陣列式」管理表單的控制項與資料,例如以下的通訊錄表單: ```typescript 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 陣列: ```html <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](https://angular.io/api/forms/FormArray) 沒有像 `splice()` 等原生 JS 方法可用。 ### FormArray 操作 使用 `push()`: ```typescript (<FormArray>this.form.get('contacts')).push( this.fb.group({ name: 'John Doe', phone: '3345678' }) ); ``` 使用 `removeAt(idx)`: ```typescript (<FormArray>this.form.get('contacts')).removeAt(idx); ``` ### patchValue 一般 form 裡面的資料可以用 `patchValue()` : ```typescript this.form.patchValue({ title: 'Another Contacts' }); ``` 對於 FormArray 資料另外用 `setControl()` 去覆蓋: ```typescript this.form.setControl( 'contacts', this.fb.array([ this.fb.group({ name: 'John Doe', phone: '3345678' }) ]) ); ``` ### Clear FormArray 要清空 FormArray 的元素,讓它長度為 0 時,用 `removeAt()` 去遞迴,寫成 function: ```typescript clearFormArray = (formArray: FormArray) => { while (formArray.length !== 0) { formArray.removeAt(0) } } ``` 另一個比較暴力的做法: ```typescript clearFormArray = (formArray: FormArray) => { formArray = this.formBuilder.array([]); } ``` 副作用是若有對 `formArray.valueChanges` 的訂閱,將會丟失 reference 而出錯,不太推薦。 ## Validations 下列是 angular 在 formGroup 中提供的 validations 的方式,當然也可以自己去寫 customer 的 validation function [Angular](https://angular.io/api/forms/Validators#compose) ```javascript 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 的設定也都拿掉,所以要特別小心** ```javascript 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 ```javascript this.formName.markAsTouched({ onlySelf: true }); ``` [Angular reactive forms set and clear validators](https://stackoverflow.com/questions/51300628/angular-reactive-forms-set-and-clear-validators) [https://www.tektutorialshub.com/angular/how-to-add-validators-dynamically-using-setvalidators-in-angular/](https://www.tektutorialshub.com/angular/how-to-add-validators-dynamically-using-setvalidators-in-angular/) ### hasError 在操作 js 的時候,需要有一個特定的 error style 可以用 `setErrors` 來設定 `boolean` 讓 `template` 來使用 ```javascript this.productsForm[index].controls["unit_price"].setErrors({ bottomPrice: true }); ``` ```html <mat-error *ngIf="productsForm[index].get('unit_price').hasError('bottomPrice')"> <span>Please enter bottomPrice.</span> </mat-error> ``` 清除不需要 error style 可以把此方法,我原本設用的 error 移除 ```javascript this.addProductForm.controls["part_no"].setErrors(null); ``` ```javascript 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 ```javascript 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)); } } ``` ```javascript 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 }))); }; } } ``` ```javascript 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](https://medium.com/@rinciarijoc/angular-custom-async-validators-13a648d688d8) ### cross reative form [Cross Field Validation Using Angular Reactive Forms - Offering Solutions Software](https://offering.solutions/blog/articles/2020/05/03/cross-field-validation-using-angular-reactive-forms/#adding-custom-validators-to-a-single-form-control) ### 自行觸發驗證 有時候的情境會需要特別用 js 來觸發驗證,就可以用此方式來確認。 ```javascript this.form.controls["part_no"].markAllAsTouched(); ``` ## Reset Reactive Form 有些情況下,submit form 之後,要在清除 form 的資料,可用下列的方式去作 clear 的動作。 ```html <form [formGroup]="addProductForm" (ngSubmit)="addProduct()" #formDirective="ngForm">...</form> ``` ```javascript 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 ```javascript this.formName.controls["control"].setValue(data); ``` ```javascript this.formName.controls["control"].patchValue(data); ``` 不管是 formGroup 和 formControl 都可以用 subscribe 去作訂閱 ```javascript this.formName.valueChanges.subscribe(()=> {...}) this.formName.controls['control'].valueChanges.subscribe(()=> {...}) ``` 如果是一個 onlySelf 的 setValue 的話,那就不會改變 `_parent` 的 value。 ```javascript // updateValueAndValidity 內容的其中一段 if (this._parent && !opts.onlySelf) { this._parent._updateTouched(opts); } ``` 目前看到[官方文件](https://angular.io/api/forms/FormGroup#setvalue)是寫 `onlySelf` 預設值為 false ,但是有些文章是 true ,這個就有待確認了,可能改過版了。 [參考 source code](https://github.com/angular/angular/blob/15.0.0/packages/forms/src/model/abstract_model.ts#L653-L667) 另外一個常用到的就是 `emitEvent` ,如果希望在 setValue 的時候不要去觸發 valueChange 的話,就可以用 ```javascript 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 參考文章](https://www.tektutorialshub.com/angular/valuechanges-in-angular-forms/) ## Problem > 如果你看到這個 error msg 的話 > [Angular reactive Form error: Must supply a value for form control with name](https://stackoverflow.com/questions/51047540/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