หลังจาก Angular v14 ออก feature ที่ผมสนใจเป็นพิเศษคือ Typed Reactive Form เรามาลองดูสิว่ามันเปลี่ยนวิธีเขียนไปมากน้อยแค่ไหน
ทำไมต้อง Type
ข้อดีของการมี type ในการเขียนโปรแกรมข้อนึงคือ เราสามารถเขียนกำหนด biz spec ลงไปใน code ได้เช่น data ก้อนนี้มี field อะไรบ้าง ประเภทเป็นตัวหนังสือหรือตัวเลข, เป็น mandatory หรือ optional ทำให้รู้ว่าเราเขียนโปรแกรมหลุดจาก biz logic ได้เลย ณ compile time หรือถ้าใช้ editor ฉลาดพอมันจะบ่นตั้งแต่เขียนเลย
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
ว่าแล้วเรามาลองรีวิวดูว่า Typed Reactive Form มาอะไรให้เราใช้กันบ้าง
น้องเล็ก FormControl
เริ่มจาก FormControl เราถ้าเราไม่ได้ประกาศ type ให้ value ก็ยังเป็น any อยู่
const searchBox = new FormControl();
const value = searchBox.value;
console.log(value) // value: any
แต่ถ้าเราเพิ่มค่าเริ่มต้นให้ type string ของ value มันจะกลายเป็น string | null ทันที และไม่สามารถ setValue เป็น type อื่นได้นอกจาก string | null
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 ไปใส่แทน
const searchBoxNotNull = new FormControl('', { nonNullable: true })
const valueNotNull = searchBoxNotNull.value;
console.log(valueNotNull) // valueNotNull: string
แต่สุดท้ายถ้าเราอยากกำหนด type แบบจริงจังว่าเป็นอะไรก็ทำได้เช่นกัน ด้วยการใส่ FormControl แบบตรง ๆ
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 จะเกิดอะไรขึ้นบ้าง
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() แทน
const stockIDRaw = stockForm.getRawValue().id; // string
const stockAbbrRaw = stockForm.getRawValue().abbr; // string
const stockAddressRaw = stockForm.getRawValue().address; // { province, zipCode }
แต่ถ้าเราพยายามตีมึนใส่ของที่ไม่ได้ spec ไว้มันจะด่าทันที ว่า name ไม่มี
กรณีที่เราระบุ spec ชัดเจนแบบนี่ทำให้ editot & compiler ช่วยเช็คได้ว่า field ที่เป็น mandaroty ต้อง remove ไม่ได้ แต่ optional ทำได้แบบนี้
เพื่อนสนิท FormBuilder
พอมี type เพิ่มเข้ามา FormBuilder เลยมี .nonNullable เพื่อช่วยสร้าง FormGroup แบบ nonNullable ได้แบบนี้
@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 ได้อีกด้วย
@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?}
}
}
แต่ถ้า spec บอกว่ายังไงก็ nonNullable เราสามารถ inject NonNullableFormBuilder เพื่อไม่ต้องคอย .nonNullable ได้ประมาณนี้
@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 ยังนึกเคสไม่ออกแต่คิดว่าคงประมาณนี้
// 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 ท่าก็จะประมาณนี้
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();
}
}
เท่าที่ลองก็จะประมาณนี้ครับ ใครลองแล้วมีอะไรมากกว่านี้ก็แนะนำกันได้นะครับ สวัสดี (-/-)
Refs:
Top comments (0)