Angular -Typed Reactive Form
Published: 2022-06-12 18:36:33

หลังจาก Angular v14 ออก feature ที่ผมสนใจเป็นพิเศษคือ Typed Reactive Form เรามาลองดูสิว่ามันเปลี่ยนวิธีเขียนไปมากน้อยแค่ไหน

# ทำไมต้อง Type
ข้อดีของการมี type ในการเขียนโปรแกรมข้อนึงคือ เราสามารถเขียนกำหนด biz spec ลงไปใน code ได้เช่น data ก้อนนี้มี field อะไรบ้าง ประเภทเป็นตัวหนังสือหรือตัวเลข, เป็น mandatory หรือ optional ทำให้รู้ว่าเราเขียนโปรแกรมหลุดจาก biz logic ได้เลย ณ compile time หรือถ้าใช้ editor ฉลาดพอมันจะบ่นตั้งแต่เขียนเลย
```ts
export interface Stock {
  id: string;
  abbr: string;
  remark?: string;
}
```
# Reactive Form เดิม
แต่ก่อน Reactive form ของ Angular ไม่ได้กำหนด type มาก่อนทำให้วิธีเขียนจะค่อนข้าง dynamic มาก ๆ แต่ก็มีข้อเสียคือเวลามีการอัปเดต field เราจะไม่รู้ทันที ใน Angular 14 โดย default จะมี type แล้ว ดังนั้นถ้าใครยังอยากใช้แบบไม่มี type ต้องเปลี่ยนไปใช้ UntypedFormControl แทน หรือท่าที่สะดวกกว่านั้นคือใช้ ng update  มันจะ convert พวก FormXXX ให้เป็น UntypedFormXXX ให้เองแบบอัตโนนาโถ

`ng update @angular/core@14 @angular/cli@14`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ypfn979iq2tbwgro6pki.png)

ว่าแล้วเรามาลองรีวิวดูว่า Typed Reactive Form มาอะไรให้เราใช้กันบ้าง

# น้องเล็ก FormControl

เริ่มจาก FormControl เราถ้าเราไม่ได้ประกาศ type ให้ value ก็ยังเป็น any อยู่

```ts
const searchBox = new FormControl();
const value = searchBox.value;
console.log(value) // value: any	
```

แต่ถ้าเราเพิ่มค่าเริ่มต้นให้ type string ของ value มันจะกลายเป็น string | null ทันที และไม่สามารถ setValue เป็น type อื่นได้นอกจาก string | null 
```ts
const searchBoxStr = new FormControl('');
const valueString = searchBoxStr.value;
console.log(valueString) // valueStr: string | null
// searchBoxStr.setValue(1) // type validate
searchBox.reset() // then searchBox.value to be null
```
ที่มันติดค่า null ไว้เพราะ FormControl มี method reset() ซึ่งโดย default แล้วมันจะ setValue กลับไป null 

ถ้า biz spec บอกว่า ค่านี้เป็น null ไม่ได้เราสามารถเพิ่ม option ให้ FormControl ไม่เอาค่า null ได้ด้วย { nonNullable: true } พอ method reset() ทำงานมันจะเอาค่าที่ init ไปใส่แทน
```ts
const searchBoxNotNull = new FormControl('', { nonNullable: true })
const valueNotNull = searchBoxNotNull.value;
console.log(valueNotNull) // valueNotNull: string
```

แต่สุดท้ายถ้าเราอยากกำหนด type แบบจริงจังว่าเป็นอะไรก็ทำได้เช่นกัน ด้วยการใส่ FormControl<string|null> แบบตรง ๆ
```ts
const searchTyped = new FormControl<string|null>('', { nonNullable: true })
const valueTyped = searchTyped.value;
console.log(valueTyped) // valueTyped: string | null
searchTyped.setValue(null);
searchTyped.setValue('Abc');
searchTyped.reset(); // reset to init value
```
 
# พี่ใหญ่ FormGroup
คราวนี้ถ้าเรา เอา FormControl ไปใส่ใน FormGroup จะเกิดอะไรขึ้นบ้าง
```ts
const stockForm = new FormGroup({
  id: new FormControl('', { nonNullable: true }),
  abbr: new FormControl('', { nonNullable: true }),
  address: new FormGroup({
    province: new FormControl('', { nonNullable: true }),
    zipCode: new FormControl('', { nonNullable: true })
  })
});

const stockID = stockForm.value.id; // string | undefine
const stockAbbr = stockForm.value.abbr; // string | undefine
const stockAddress = stockForm.value.address; // Partial<{ province, zipCode}> | undefine
```

ถ้าเราสังเกตจะเห็นว่าเวลาเราเอา stockID หรือ stockAbbr มันจะได้ค่าเป็น string | undefine เพราะเวลาเรา disable FormControl ที่อยู่ใน FormGroup มันจะทำให้ค่าเป็น undefine ในทางเดียวกัน ถ้าเป็น Nested FormGroup ที่ซ้อนลงไปค่าที่ได้ก็จะเป็น Partial เพราะมันมีโอกาสที่จะเป็น undefine และได้ field มาไม่ครบได้เหมือนกัน

ถ้า spec บอกว่าอยากได้ field ครบเสมอ เราใช้ getRawValue() แทน
```ts
const stockIDRaw = stockForm.getRawValue().id; // string
const stockAbbrRaw = stockForm.getRawValue().abbr; // string
const stockAddressRaw = stockForm.getRawValue().address; // { province, zipCode }
```
แต่ถ้าเราพยายามตีมึนใส่ของที่ไม่ได้ spec ไว้มันจะด่าทันที ว่า name ไม่มี
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9p4jn9n5y5lcb88u9pz2.png)

 ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3mzj5by5i0od8p5tiqwg.png)

กรณีที่เราระบุ spec ชัดเจนแบบนี่ทำให้ editot & compiler ช่วยเช็คได้ว่า field ที่เป็น mandaroty ต้อง remove ไม่ได้ แต่ optional ทำได้แบบนี้

# เพื่อนสนิท FormBuilder
พอมี type เพิ่มเข้ามา FormBuilder เลยมี .nonNullable เพื่อช่วยสร้าง FormGroup แบบ nonNullable ได้แบบนี้ 

```ts
@Component({})
export class StockComponent {

  protected stockFormFb = this.fb.nonNullable.group({
    id: '',
    abbr: ''
  });

  constructor(private fb: FormBuilder) {
    this.stockFormFb.value; // Partail<{id, abbr}>
    this.stockFormFb.getRawValue(); {id, abbr}
  }
```

นอกจากนี้ FormBuilder ยังมีท่าที่น่าสนใจอีกท่านึงคือเราสามารถส่ง interface ที่เป็น model spec ลงไปได้ ทำให้การล็อก spec การสร้าง FormGroup ได้อีกด้วย
```ts
@Component({})
export class StockComponent {

  protected stockFormFbInterface = this.fb.nonNullable.group<Stock>({
    id: '',
    abbr: '',
  });

  constructor(private fb: FormBuilder) {
    this.stockFormFbInterface.value; // Partail {id, abbr, remark?}
    this.stockFormFbInterface.getRawValue(); // {id, abbr, remark?}
  }
}
```
ถ้ามีของแปลกปลอมจะแดงทันที
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/susc04hds0mshcsimksd.png)

 แต่ถ้า spec บอกว่ายังไงก็ nonNullable เราสามารถ inject NonNullableFormBuilder เพื่อไม่ต้องคอย .nonNullable ได้ประมาณนี้ 
 
 ```ts
@Component({
  selector: 'app-stock',
  template: ``
})
export class StockComponent {

  protected stockFormNnInterface = this.nnFb.group<Stock>({
    id: '',
    abbr: '',
  })

  constructor(private nnFb: NonNullableFormBuilder) {
    this.stockFormNnInterface.value;
  }
}
```
# น้องใหม่ FormRecord
บางครั้งเราอยากจะเจอโจทย์ประมาณอยากให้ Form ของเรามี filed dynamic ณ runtime ยังนึกเคสไม่ออกแต่คิดว่าคงประมาณนี้
```ts
// runtime data
const stocks: Stock[] = [
  { id: 'BKK.ABC', abbr: 'ABC' },
  { id: 'CGM.ABC', abbr: 'ABC' },
  { id: 'BKK.DGF', abbr: 'DEF' }
];
// runtime init
const topStockRecord = new FormRecord<FormControl<string>>({});
stocks.forEach(stock =>
  topStockRecord.addControl(
    stock.id,
    new FormControl(stock.abbr, { nonNullable: true })
  )
);
```

# ของแถม
จาก blog @netbasal เขาแถม type ที่ชื่อว่า ControlsOf (มาจาก lib ที่ชื่อ ngneat/reactive-forms) เพื่อให้สามารถกำหนด interface ให้เราจะได้เลยตอน new FormGroup ท่าก็จะประมาณนี้
```ts
export type ControlsOf<T extends Record<string, any>> = {
  [K in keyof T]: T[K] extends Record<any, any>
  ? FormGroup<ControlsOf<T[K]>>
  : FormControl<T[K]>;
};

@Component({
  selector: 'app-stock',
  template: ``
})
export class StockComponent {
  protected stockControlOf = new FormGroup<ControlsOf<Stock>>({
    id: new FormControl('', {nonNullable: true}),
    abbr: new FormControl()
  })

  constructor() {
    this.stockControlOf.value;
    this.stockControlOf.getRawValue();
  }
}
```
บ่นทันทีที่ field ไม่มี
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/42166n2jmijvgm0aq0pz.png)

 เท่าที่ลองก็จะประมาณนี้ครับ ใครลองแล้วมีอะไรมากกว่านี้ก็แนะนำกันได้นะครับ สวัสดี (-/\-)

Refs: 
- https://netbasal.com/typed-reactive-forms-in-angular-no-longer-a-type-dream-bf6982b0af28
- https://angular.io/guide/typed-forms