ControlValueAccessor ngModel 未更新

ControlValueAccessor ngModel not being updated

这是简单的自定义表单控件

@Component({
  selector: 'app-custom-control',
  template: `
    {{ value }}
    <input [ngModel]="value" (ngModelChange)="onChange($event)">
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomControlComponent),
    multi: true,
  }]
})
export class CustomControlComponent implements ControlValueAccessor {

  private value: any;

  private onChange: (val) => void;
  private onTouch: () => void;

  writeValue(value: any) {
    this.value = value;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouch = fn;
  }
}

使用如下:

@Component({
  selector: 'my-app',
  template: `
    <app-custom-control
      [ngModel]="model"
      (ngModelChange)="onChange($event)">
    </app-custom-control>
    <input [ngModel]="model" (ngModelChange)="onChange($event)">
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  model = 'hello';

  onChange(value) {
    this.model = value;
  }
}

我不明白的是为什么控件的ngModel只根据外部输入的变化值进行更新,而不是在使用内部输入的情况下? 现场示例:https://stackblitz.com/edit/angular-7apjhg

编辑:

实际问题可以通过更简单的例子看到(没有内部输入):

@Component({
  selector: 'app-custom-control',
  template: `
    {{ value }}
    <button (click)="onChange('new value')">set new value</button>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomControlComponent),
    multi: true,
  }]
})
export class CustomControlComponent implements ControlValueAccessor {

  value: any;

  onChange: (val) => void;
  onTouched: () => void;

  writeValue(value: any) {
    this.value = value;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
}

单击自定义控件内的按钮后,属性 父项上的值会更新,但 ngModel 不会。更新示例:https://stackblitz.com/edit/angular-tss2f3

为了使其正常工作,您必须对 custom-control.component.ts

中的输入使用 box 语法

自定义-control.component.ts

<input [(ngModel)]="value" (ngModelChange)="onChange($event)">

Working Example.


发生这种情况是因为当您在外部输入中输入时,CustomControlComponentControlValueAccessor.writeValue() 将被执行,这反过来将更新内部输入。

让我们把它分解成更小的步骤。

1) 输入外部输入

2) 触发变化检测

3) 来自 NgModel 指令(绑定到 custom-control)的 ngOnChanges 最终将到达,这将导致 FormControl 实例在下一个报价

@Directive({
  selector: '[ngModel]:not([formControlName]):not([formControl])',
  providers: [formControlBinding],
  exportAs: 'ngModel'
})
export class NgModel extends NgControl implements OnChanges,
    OnDestroy {
 /* ... */
 ngOnChanges(changes: SimpleChanges) {
    this._checkForErrors();
    if (!this._registered) this._setUpControl();
    if ('isDisabled' in changes) {
        this._updateDisabled(changes);
    }

    if (isPropertyUpdated(changes, this.viewModel)) {
        this._updateValue(this.model);
        this.viewModel = this.model;
    }

  /* ... */

 private _updateValue(value: any): void {
    resolvedPromise.then(
        () => { this.control.setValue(value, { emitViewToModelChange: false }); 
    });
  }
 }
}

4) FormControl.setValue() 将调用已注册的更改函数回调,后者将依次调用 ControlValueAccessor.writeValue

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

    // control -> ngModel
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  });

其中 dir.valueAccessor !.writeValue(newValue) 将是 CustomControlComponent.writeValue 函数。

writeValue(value: any) {
    this.value = value;
}

这就是您的内部输入被外部输入更新的原因。


现在,为什么反过来不行呢?

当您在内部输入中键入内容时,它将仅调用它的onChange函数,它看起来像这样:

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);
  });
}

这又将是 updateControl 函数。

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;
}

查看 updateControl 内部,您会看到它有 { emitModelToViewChange: false } 标志。窥视 FormControl.setValue(),我们会看到该标志阻止更新内部输入。

setValue(value: any, options: {
    onlySelf?: boolean,
    emitEvent?: boolean,
    emitModelToViewChange?: boolean,
    emitViewToModelChange?: boolean
  } = {}): void {
    (this as{value: any}).value = this._pendingValue = value;

    // Here!
    if (this._onChange.length && options.emitModelToViewChange !== false) {
      this._onChange.forEach(
          (changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
    }
    this.updateValueAndValidity(options);
  }

事实上,只有内部输入没有更新,但是绑定到该输入的 FormControl 实例被更新了。这可以通过这样做看到:

自定义-control.component.html

{{ value }}

<input #i="ngModel" [ngModel]="value" (ngModelChange)="onChange($event)">

{{ i.control.value | json }} <!-- Always Updated -->