当配置为不配置时,自定义 Angular FormField 发出事件

Jac*_*ack 8 rxjs typescript angular-material angular

这个问题有一个Stackblitz:stackblitz.com/edit/angular-material-starter-template-8ojscv

我已经实现了一个包装CodeMirror的自定义 Angular Material FormField

在我的应用程序组件中,我订阅表单控件上的valueChanges以监听用户键入:

export class AppComponent implements OnInit {
   // Custom value accessor for CodeMirror.
   control: FormControl = new FormControl('', {updateOn: 'change'});

   ngOnInit() {
    // Listen for the user typing in CodeMirror.
    this.control.valueChanges.pipe(
        debounceTime(500),
        distinctUntilChanged(),
        tap((value: string) => {
          console.log(`The user typed "${value}"`);
        })
    ).subscribe();
  }
}
Run Code Online (Sandbox Code Playgroud)

我注意到,当使用setValue时,valueChanges Observable 会发出一个值,即使选项对象禁止它

// This appears to have no effect.
this.control.setValue(value, {
   // Prevent the statusChanges and valueChanges observables from
   // emitting events when the control value is updated.
   emitEvent: false,
}
Run Code Online (Sandbox Code Playgroud)

我的 Stackblitz 演示中的高级流程是:

  1. 单击setValue(0)按钮
  2. 应用程序组件调用 setValueFormControlemitEvent: false
  3. 在自定义FormField writeValue(value: string)组件上调用 ControlValueAccessor 方法( my-input.component)
  4. 写入值被委托给MatFormFieldControl value setter
  5. MatFormFieldControl setter value值写入 CodeMirror 编辑器this._editor.setValue(value + "")
  6. CodeMirrorchanges 事件被触发(已在 中添加ngAfterViewInit)。
  7. 使用更新后的值调用注册的回调函数 ( this._onChange(cm.getValue()))
  8. ObservablevalueChanges发出更新后的值

是的,my-input.component显式调用注册的回调函数,但我预计框架(Angular 或 Angular Material)会尊重emitEvent: false而不发出事件。

自定义 FormField 实现是否有责任实现选项对象,并且如果emitEvent: false已设置,则不调用已注册的回调?

And*_*tej 5

我认为问题来自于codemirrorValueChanged

codemirrorValueChanged(
  cm: CodeMirror.Editor,
  change: CodeMirror.EditorChangeLinkedList
) {
  if (change.origin !== "setValue") {
    console.log(`_onChange(${this.value})`);
    this._onChange(cm.getValue());
  }
}
Run Code Online (Sandbox Code Playgroud)

但首先,让我们看看发生了什么FormControl.setValue()

setValue(value: any, options: {
  onlySelf?: boolean,
  emitEvent?: boolean,
  emitModelToViewChange?: boolean,
  emitViewToModelChange?: boolean
} = {}): void {
  (this as {value: any}).value = this._pendingValue = value;
  if (this._onChange.length && options.emitModelToViewChange !== false) {
    this._onChange.forEach(
        (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
  }
  this.updateValueAndValidity(options);
}
Run Code Online (Sandbox Code Playgroud)

无论您使用反应式表单还是模板表单,都必须设置每个控件,为此我们有函数_setupControlNgModelFormControlName),它具有不同的实现,具体取决于指令,但在每种情况下它最终都会称呼setUpControl

export function setUpControl(control: FormControl, dir: NgControl): void {
  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]);
  // `writeValue`: MODEL -> VIEW
  dir.valueAccessor!.writeValue(control.value);

  setUpViewChangePipeline(control, dir);
  setUpModelChangePipeline(control, dir);

  setUpBlurPipeline(control, dir);

  if (dir.valueAccessor!.setDisabledState) {
    /* ... */
  }

  /* ... */
}
Run Code Online (Sandbox Code Playgroud)

ThesetUpViewChangePipeline是被调用的ControlValueAccessor地方:registerOnChange

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
  dir.valueAccessor!.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingChange = true;
    control._pendingDirty = true;

    // `updateControl` - update value from VIEW to MODEL
    // e.g `VIEW` - an input
    // e.g `MODEL` - [(ngModel)]="componentValue"
    if (control.updateOn === 'change') updateControl(control, dir);
  });
}
Run Code Online (Sandbox Code Playgroud)

setUpModelChangePipeline是填充_onChange数组(来自setValue代码片段)的位置:

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);
  });
}
Run Code Online (Sandbox Code Playgroud)

因此,这就是emitModelToViewChange标志 (from options.emitModelToViewChange !== false) 很重要的地方。

接下来,我们有updateValueAndValidity,这是valueChangesstatusChanges主题发出的地方:

updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
  this._setInitialStatus();
  this._updateValue();

  if (this.enabled) {
    // In case of async validators
    this._cancelExistingSubscription();

    // Run sync validators
    (this as {errors: ValidationErrors | null}).errors = this._runValidator();
    (this as {status: string}).status = this._calculateStatus();

    if (this.status === VALID || this.status === PENDING) {
      this._runAsyncValidator(opts.emitEvent);
    }
  }

  // !
  if (opts.emitEvent !== false) {
    (this.valueChanges as EventEmitter<any>).emit(this.value);
    (this.statusChanges as EventEmitter<string>).emit(this.status);
  }

  if (this._parent && !opts.onlySelf) {
    this._parent.updateValueAndValidity(opts);
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,我们可以得出结论,问题并非源于FormControl.setValue(val, { emitEvent: false }).

updateValueAndValidity在调用之前,我们看到_onChange函数将首先被调用。同样,这样的函数看起来像这样:

// From `setUpModelChangePipeline`
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
  // control -> view
  dir.valueAccessor!.writeValue(newValue);

  // control -> ngModel
  if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
Run Code Online (Sandbox Code Playgroud)

在我们的例子中,valueAccessor.writeValue看起来像这样:

writeValue(value: string): void {
    console.log(`[ControlValueAccessor] writeValue(${value})`);
    // Updates the Material UI value with `set value()`.
    this.value = value;
  }
Run Code Online (Sandbox Code Playgroud)

这将调用设置器:

set value(value: string | null) {
  console.log(`[MatFormFieldControl] set value(${value})`);
  if (this._editor) {
    this._editor.setValue(value + "");
    this._editor.markClean();
    // Postpone the refresh() to after CodeMirror/Browser has updated
    // the layout according to the new content.
    setTimeout(() => {
      this._editor.refresh();
    }, 1);
  }
  this.stateChanges.next();
}
Run Code Online (Sandbox Code Playgroud)

由于_editor.setValue,该onChanges事件将发生并被codemirrorValueChanged调用:

codemirrorValueChanged(
  cm: CodeMirror.Editor,
  change: CodeMirror.EditorChangeLinkedList
) {
  if (change.origin !== "setValue") {
    console.log(`_onChange(${this.value})`);
    this._onChange(cm.getValue());
  }
}
Run Code Online (Sandbox Code Playgroud)

_onChange调用这个回调有什么作用:

// from `setUpViewChangePipeline`
dir.valueAccessor!.registerOnChange((newValue: any) => {
  control._pendingValue = newValue;
  control._pendingChange = true;
  control._pendingDirty = true;

  if (control.updateOn === 'change') updateControl(control, dir);
});
Run Code Online (Sandbox Code Playgroud)

updateControl会打电话control.setValue,但没有 emitEvent: false

function updateControl(control: FormControl, dir: NgControl): void {
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
  dir.viewToModelUpdate(control._pendingValue);
  control._pendingChange = false;
}
Run Code Online (Sandbox Code Playgroud)

因此,这应该可以解释当前的行为。


我在调试时发现的一件事是,这change是一个数组,而不是一个对象。

因此,一个可能的解决方案是:

codemirrorValueChanged(
  cm: CodeMirror.Editor,
  change: CodeMirror.EditorChangeLinkedList
) {
  if (change[0].origin !== "setValue") {
    console.log(`_onChange(${this.value})`);
    this._onChange(cm.getValue());
  }
}
Run Code Online (Sandbox Code Playgroud)

我试图在Angular Forms 的彻底探索中解释这些概念以及 Angular Forms 内部结构是如何工作的。