---
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