# [Angular] Angular Reactive Form ###### tags: `Angular` `前端筆記` ## Reactive Form 相較於另一種 Angular Form 形式 [Template-driven-form](https://angular.io/guide/forms),使用 Reactive Form 可以讓 Template 乾淨一些,因為 Reactive Form 需要開發者自行建立 Angular 提供的實例(instance),再同步綁定 template 上才可以控制表單(form)。 ## 必須在 root module `import` `ReactiveFormsModule` 要不然就沒辦法使用 Angular 的 Reactive Form: ```typescript= // src/app/app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; ... @NgModule({ declarations: [...], imports: [ BrowserModule, FormsModule, ReactiveFormsModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` ## Angular 提供的表單控制 Angular 提供下列的 `class`,依照這些 `class` 產生的實例物件(instance)可以幫助開發者管控表單的狀態: > 注意:記得要在使用的 component 中 `import` 要是用的 `class` ![](https://hackmd.io/_uploads/BJRrlPQFq.png) (基本上就是 `FormGroup` -> `FormControl` / `FomrArray` / `FormGroup` 的無限延伸) ### 1. `FormControl`: Reactive Form 中最基本的單位,用來==封裝 form 中單一個欄位==,封裝 + 同步綁定 template 後就可以得到該欄位的實例物件(instance)。 `FormControl` 必須存在與 `FormGroup` 之內,不可以單獨使用,要不然就會報錯: ![](https://hackmd.io/_uploads/Byf-DL7F9.png) #### 得到該欄位的實例物件可以幹嘛? 開發者就可以得到其他實用的屬性及方法(比如說 `invalid`, `touched`)等等,來知道欄位狀態,並依照欄位狀態決定該如何處理表單(e.g. 未通過表單驗證不可以 submit the form)。 ### 2. `FormGroup`: - `FormControl` 為表單(form)中單一個欄位,`FormGroup` 則是代表欄位的家,也就是表單(form) - `FormGroup` 接收 `FormControl`, `FormArray` 及 `FormGroup`( -> nested FormGroup),並且在自身的 `controls` 保存它們 - key name 為個別 `FormControl`, `FormArray` 及 `FormGroup` 的 key name - `FormGroup` 可以監察每個 child,如果一個 `FormControl` 沒有通過表單驗證,那麼 `FormGroup` 自身的 `valid` 就為 `false` - 會確保 form 中的每一個 child 都沒有問題,那麼 form 才會沒有問題 #### `FormGroup` 可以再包一個 `FormGroup` 嗎? 可以。`FormGroup` 可以再包 `FormGroup` 形成巢狀(nested),但是同步綁定 template 時 + 取得 `FormGroup` 時要注意結構。 ### 3. `FormArray`: - `FormArray` 可以接受 `FormControl`, `FormGroup` 或者 `FormArray`(-> nested FormArray) - 基本上就是陣列版本的 FormGroup -> `[FormGroup, FormGroup, FormGroup]` - 需要動態新增 / 刪除表單欄位就需要使用 `FormArray` 取得陣列的特性刪減資料 #### `FormArray` 可以再包一個 `FormArray` 嗎? 可以,但是 template 的結構要特別注意。 ## 簡單的小例子 > 流程:依照欄位決定 Form 結構 -> 在 typescript 建立 form 結構 -> 在 template 綁定 - user name 欄位 input 且必填 - user email 欄位 input 且必填 + 驗證 email - 有性別的 radio - 可以自行新增 / 刪除地址的欄位 ![](https://hackmd.io/_uploads/Syo0MvQYc.png) (沒通過表單驗證就不能 submit) ![](https://hackmd.io/_uploads/Hy8yXwXY5.png) (可以自行新增多筆地址的欄位 / 刪除) ### 建立 Reactive Form 的表單控制 - 到 `init` 的時候才初始化表單(公司 coding style) - 用 `getter` 可以簡化取得單個 `FormControl` / `FormArray` 的麻煩 - 要不然就要一直寫 `this.myForm.get('formControlName')` - `FormArray` 給予使用者自行新增 / 刪除欄位的功能 - `(<FormArray>this.form.get('arrayName')).method` -> 讓 typescript 知道我們是在使用 `FormArray` - 推 `FormGroup` 進 `FormArray` ```typescript= import { Component, OnInit } from '@angular/core'; import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { // 需要加 ! 逃避 typescript 的型別檢查,以便順利在 init 的時候初始化 myForm!: FormGroup; genders: string[] = ['male', 'female']; get userNameControl(): FormControl { return this.myForm.get('userName') as FormControl; } get userEmailControl(): FormControl { return this.myForm.get('userEmail') as FormControl; } get userLocationControlArray(): FormArray { return this.myForm.get('userLocations') as FormArray } constructor() {} ngOnInit() { // 表單內由 control + array 組成 this.myForm = new FormGroup({ userName: new FormControl(null, Validators.required), userEmail: new FormControl(null, [Validators.required, Validators.email]), userGender: new FormControl('male'), userLocations: new FormArray([]) }); } onSubmit(): void { console.log(this.myForm); } // 取得單個 FormControl,並依照 status(表單欄位目前驗證的結果)顯示不同的 feedback getInvalidFeedback(formControl: FormControl): string { const { pristine, untouched }: { pristine: boolean, untouched: boolean } = formControl; let invalidFeedback: string = ''; if (pristine && untouched) invalidFeedback = ''; else if (formControl.errors?.['required']) { invalidFeedback = 'required'; } else if (formControl.errors?.['email']) { invalidFeedback = 'value is invalid'; } return invalidFeedback; } // 在 FormArray 新增 FormGroup -> 使用者可新增 / 刪除多個地址欄位 onAddSubForm(): void { // 推 FormGroup 進 FormArray const locationGroup: FormGroup = new FormGroup({ userCountry: new FormControl(null, Validators.required), userCity: new FormControl(null, Validators.required) }); // NOTE: 需要用 (<FormArray>this.myForm.get('controlName')) -> typescript 才會知道這個是一個 formArray // (<FormArray>this.myForm.get('userLocations')).push(locationGroup); // NOTE: getter 的幫助省略程式碼 this.userLocationControlArray.push(locationGroup) } onDeleteSubForm(index: number): void { // NOTE: Angular 的 FormArray 沒有 Array.splice 這個方法 // (<FormArray>this.myForm.get('userLocations')).removeAt(index); // NOTE: getter 的幫助省略程式碼 this.userLocationControlArray.removeAt(index); } } ``` ### 同步綁定 template(處理 view 的部分) - `<form>` 要用 `[formGroup]` 綁定 fromGroup 的 key name` - `(ngSubmit)` Angular form 提供的 submit event - `formControlName` 代表 `formGroup` 內的 `fomrControl`,一樣綁定 key name - `[formGroup]` -> `formControlName` / `formArrayName` - nested: `[formGroup]` -> `[formGroupName]` -> `formControlName` - 因為 `controls` 才是存放 `FormArray` 中的 `control`, `group` or `array`,所以用 `getter.controls` -> `formArray.controls` - 有推 `FormGroup` 進 `FormArray` 內,所以要再把 `control` 從 `group` 中取出 - 必須拿 index,告訴 Angular 是陣列中的哪一組(也就是 value)要被渲染 `[FormGroupName]="index"` - 為了讓 `*ngFor` 的 `<label>` `<input>` 正確連結,必須 property binding + old school way 組裝 string,因為 Angular property binding 不能用 Template literals ```htmlmixed= <form [formGroup]="myForm" (ngSubmit)="onSubmit()" > <div> <label for="username">Username</label> <input type="text" id="username" formControlName="userName" /> <!-- NOTE: 表單驗證的回饋 --> <!-- <div> --> <!-- <span class="invalid-feedback">{{ getInvalidFeedback(userNameControl) }}</span> --> <!-- </div> --> <!-- NOTE: 或者寫在 template 裡面 --> <div *ngIf="myForm.get('userName')?.invalid && myForm.get('userName')?.touched" > <span>Hello</span> </div> </div> <div> <label for="email">email</label> <input type="text" id="email" formControlName="userEmail" /> <div> <span class="invalid-feedback">{{ getInvalidFeedback(userEmailControl) }}</span> </div> </div> <div *ngFor="let gender of genders"> <label> <input type="radio" [value]="gender" formControlName="userGender" />{{ gender }} </label> </div> <div formArrayName="userLocations" > <!-- getter 簡化取得 formArray.controls --> <!-- 要不然得寫 formGroup.get('formArray').controls --> <div *ngFor="let location of userLocationControlArray.controls; let i = index" > <!-- 必須用 index 當作 formGroupName,這樣子才可以區別 array 的 value --> <div [formGroupName]="i"> <!-- [property binding]="expression" 但是 angular 不支援 `${}` 所以必須 old school way --> <label [for]="'country'+i">User Country:</label> <input type="text" formControlName="userCountry" [id]="'country'+i" /> <label [for]="'city'+i">City:</label> <input type="text" formControlName="userCity" [id]="'city'+i" /> </div> <button type="button" (click)="onDeleteSubForm(i)" > Delete this location form </button> </div> </div> <div> <button type="button" (click)="onAddSubForm()" > Add location form </button> </div> <button class="btn btn-primary" type="submit" [disabled]="myForm.invalid" >Submit</button> </form> ``` ## 查看 form 的物件 ### 1. `formGroup.controls` 取得 group 內 child 的物件 因為在 `init` 的時候有給 `myForm` 值,`form.controls` 會取得 form 的 child: ```typescript= ngOnInit() { this.myForm = new FormGroup({ userName: new FormControl(null, Validators.required), userEmail: new FormControl(null, [Validators.required, Validators.email]), userGender: new FormControl('male'), userLocations: new FormArray([]) }); } ``` ![](https://hackmd.io/_uploads/Hk3HTDXFq.png) 只要其中一個 child 為 invalid 那麼 group 就會是 invalid: ![](https://hackmd.io/_uploads/ByiMAvQt9.png) ### 2. 取 child 記得要透過 `get('targetControlKeyName')` 或者 `group.controls.targetControlKeyName` 雖然看起來就像是 ```javascript= myForm: { userName: {...}, userEmail: {...}, userGender: {...}, userLocations: {...}, } ``` 感覺也可以 `myForm.userName` 得到 targetControl(當然不行,會報錯),要用 `get(targetControlKeyName)` 或者 `group.controls.targetControlKeyName` 拿 control 才可以。 ### 3. `setValue` 及 `patchValue`,可以不透過 UI 給 form 值 ==都不會變更 control 的 pristine== #### `sevValue` 每個 keys 都很重要 `setValue` 一次就是單個,或者全部,沒辦法少東少西: > 所以只給一個的時候 + 帶入 API 後新增表單的預設值就用 `setValue` -> 前輩:良好的 coding style ```typescript= onSetValue(): void { // ok! 因為只給一個 this.myForm.get('userName')?.setValue('hello'); console.log(this.myForm.get('userName')); } ``` username 的值為 hello,且不會更動 `pristine`: ![](https://hackmd.io/_uploads/Byxgzu7Kq.png) ```typescript= onSetValue(): void { // error! all keys ara important! this.myForm.setValue({ userName: 'hello' }); } ``` ![](https://hackmd.io/_uploads/rJAwMdQtc.png) ### `patchValue` 都沒關係,只要有給對就好 ```typescript= onPatchValue(): void { this.myForm.patchValue({ userName: 'hello' }); console.log(this.myForm.get('userName')); } ``` 更新 username 後也不會更動到 `pristine`: ![](https://hackmd.io/_uploads/SJxuXdQtc.png) <hr /> 需要再更新 form status! form array -> form builder 等等 也許練習的可以丟到 GitHub 當 reference ## 參考資料 1. [Angular - The Complete Guide (2022 Edition)](https://www.udemy.com/course/the-complete-guide-to-angular-2/learn/lecture/6656502#questions/3829912) -> lec. 202 - 217 2. [Reactive forms](https://angular.io/guide/reactive-forms) 3. [[從 0 開始的 Angular 生活]No.48 Angular 響應式表單 (Reactive forms) (一)](https://pvt5r486.github.io/f2e/20190618/1504140932/) 4. [Angular Reactive Forms Example](https://www.tektutorialshub.com/angular/angular-reactive-forms/) -> 內有基礎範例 5. [Nested FormArray Example Add Form Fields Dynamically](https://www.tektutorialshub.com/angular/nested-formarray-example-add-form-fields-dynamically/) -> 內有 nested `FormArray` 範例 6. [How To Use Reactive Forms in Angular](https://www.digitalocean.com/community/tutorials/angular-reactive-forms-introduction) -> 內有前輩要求的 form 在 `init` 再初始化(給值)的範例 7. [Angular 深入淺出三十天:表單與測試 Day03 - Reactive Forms 實作 - 以登入為例](https://ithelp.ithome.com.tw/articles/10266617) 8. [How to Use Angular FormArray(s) within FormGroup(s) In Reactive Forms](https://dontpaniclabs.com/blog/post/2022/01/05/how-to-use-angular-formarrays-within-formgroups-in-reactive-forms/)