# [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`

(基本上就是 `FormGroup` -> `FormControl` / `FomrArray` / `FormGroup` 的無限延伸)
### 1. `FormControl`:
Reactive Form 中最基本的單位,用來==封裝 form 中單一個欄位==,封裝 + 同步綁定 template 後就可以得到該欄位的實例物件(instance)。
`FormControl` 必須存在與 `FormGroup` 之內,不可以單獨使用,要不然就會報錯:

#### 得到該欄位的實例物件可以幹嘛?
開發者就可以得到其他實用的屬性及方法(比如說 `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
- 可以自行新增 / 刪除地址的欄位

(沒通過表單驗證就不能 submit)

(可以自行新增多筆地址的欄位 / 刪除)
### 建立 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([])
});
}
```

只要其中一個 child 為 invalid 那麼 group 就會是 invalid:

### 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`:

```typescript=
onSetValue(): void {
// error! all keys ara important!
this.myForm.setValue({
userName: 'hello'
});
}
```

### `patchValue` 都沒關係,只要有給對就好
```typescript=
onPatchValue(): void {
this.myForm.patchValue({ userName: 'hello' });
console.log(this.myForm.get('userName'));
}
```
更新 username 後也不會更動到 `pristine`:

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