最近腦細胞死的有點快,標題改成「微探討」壓力比較沒那麼大。有時候甚至覺得當初名稱應該要取個「你可以不用知道的 Angular」才對 ?
今天的內容是要來還距今差不多一個禮拜前([Day 7] FormGrop、FormControlName 與 FormControl 之間的秘密)留下的文章債。
在第七天的文章最後,稍微把 FormControl 與 HTML input element value 兩者之間怎麼互動的內容給省掉,今天就要來揭開他們的神秘面紗。
// HTML Template
<form [formGroup]="someGroup">
<input formControlName="aInput">
<button type="submit" (click)="onSubmit()">Submit</button>
</form>
// TypeScript
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
someGroup: FormGroup;
title = 'day15';
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.someGroup = this.fb.group({
aInput: new FormControl(),
});
}
onSubmit(): void {
console.log(`⚡: AppComponent -> onSumbit `, this.someGroup.value);
}
}
↑ Block 1
Block 1 中我簡單的建立了一個 Reactive Forms,接下來就是重點,還記得 Day 7 提到的 addControl 嗎?今天會從這個地方作為起點:
addControl(dir: FormControlName): FormControl {
const ctrl: any = this.form.get(dir.path);
**setUpControl(ctrl, dir);**
ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir);
return ctrl;
}
↑ Block 2
Block 2 是 FormGroup 的 addControl 方法,它會呼叫 shared.ts 內的 setUpControl 函式:
export function setUpControl(control: FormControl, dir: NgControl): void {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
}
control.validator = Validators.compose([control.validator!, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator!, dir.asyncValidator]);
dir.valueAccessor!.writeValue(control.value);
// ... 下接 Block 5
}
↑ Block 3
當進到 Block 3 的 setUpControl 時,傳入的參數分別會是:
接著按照程式進展,可以發現 Angular 會開始將 dir 與 control 身上的 validator、asyncValidator compose 起來,並重新指派給 control 的 validator 與 asyncValidator。
再來會透過 dir.valueAccessor 來將 control 的 value 寫入 host element。
回頭看一下 FormControlName 的程式碼,可以發現這個 valueAccessor 是使用 NG_VALUE_ACCESSOR
這個 multi DI Token 來注入的。一般當 FormControlName 是放在 input element 身上的情境下,FormControlName directive 所取得的實體會是:DefaultValueAccessor 這個物件的實體。
這些 ControlValueAccessor 介面的物件(不只 DefaultValueAccessor),簡單來說是用來作為 FormControl 與 host element 的橋樑。
像前面有提到 Angular 在處理完 validator 之後,會將 control 的 value 傳給 host element,它所呼叫的 writeValue 方法背後所做的事長這樣:
writeValue(value: any): void {
const normalizedValue = value == null ? '' : value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
↑ Block 4
單純就是透過 Renderer2 物件的 setProperty 方法來把 value assign 給 host element(this._elementRef 是透過 DI 的方式取得,於 directive 使用會取得其 host element)。
接著就可以往下繼續看 Block 5:
setUpViewChangePipeline(control, dir);
setUpModelChangePipeline(control, dir);
// ... 略
↑ Block 5
接著 Angular 會處理來自 view 的 value change,只要當 value 有變動時,就應該要同時更新跟它綁在一起的 FormContorl:
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor!.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
↑ Block 6:setUpViewChangePipeline
從上方 Block 6 可以看到 Angular 呼叫了 valueAccessor 的 registerOnChange 方法,如果你有不小心跳出文章去偷看一下 DefaultValueAccessor 的實作,你應該會發現這個 registerOnChange 方法,其實只是將傳入的 function 指派給 DefaultValueAccessor 的 onChange 屬性(link)。
以 DefaultValueAccessor 來講,只要當 input 元素發出 compositionend 或是 input 事件,就會執行 onChange 屬性指向的 function,也就是 Block 6 傳入的箭頭函式。
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor!.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
↑ Block 7:setUpModelChangePipeline
Block 7 的邏輯剛好與 Block 6 相反,當使用 FormContorl 的 setValue 方法,就會呼叫 Block 7 傳入的箭頭函式,透過 DefaultValueAccessor 來更新 input element 的 value 屬性。
那麼以上就是今天「你可以不用知道的 Angular」!(並不是)
預祝各位中秋吃好、連假睡飽 ?
以下按照入團順序列出我們團隊夥伴的系列文章!