iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
Modern Web

從巨人的 Tip 看 Angular系列 第 15

[Day 15] 微探討 FormControl 跟 HTML input element 的 value 怎麼互動

  • 分享至 

  • xImage
  •  

最近腦細胞死的有點快,標題改成「微探討」壓力比較沒那麼大。有時候甚至覺得當初名稱應該要取個「你可以不用知道的 Angular」才對 ?


今天的內容是要來還距今差不多一個禮拜前([Day 7] FormGrop、FormControlName 與 FormControl 之間的秘密)留下的文章債。

在第七天的文章最後,稍微把 FormControl 與 HTML input element value 兩者之間怎麼互動的內容給省掉,今天就要來揭開他們的神秘面紗。

直接進 code 沒在等

// 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 時,傳入的參數分別會是:

  1. control:FormControl 類別的物件,以本例來說,就是被指派給 aInput 的那個 FromControl 實體。
  2. dir:NgControl 類別的物件,以本例來說是放在 input 元素上的 FormControlName directive 實體。

接著按照程式進展,可以發現 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」!(並不是)

預祝各位中秋吃好、連假睡飽 ?

中秋前夕 ?

以下按照入團順序列出我們團隊夥伴的系列文章!


上一篇
[Day 14] * 與 microsyntax
下一篇
[Day 16] Not a Tip but a Trick!使用其他符號來做 interpolation 吧
系列文
從巨人的 Tip 看 Angular30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言