2

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

@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

4

1 回答 1

9

为了使其工作,您必须使用box 语法中的香蕉作为驻留在内部的输入custom-control.component.ts

自定义控件.component.ts

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

工作示例


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

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

1) 输入外部输入

2) 触发变更检测

3)最终会到达ngOnChangesfromNgModel指令(即绑定到custom-control),这将导致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;
}

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


现在,为什么它不能反过来工作呢?

When you're typing into the inner input, it will only invoke its onChange function, which would look like this:

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

Which will again the updateControl function.

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

Looking inside updateControl, you'll see that it has the { emitModelToViewChange: false } flag. Peeking into FormControl.setValue(), we'll see that the flag prevents the inner input from being updated.

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

In fact, only the inner input is not updated, but the FormControl instance bound to that input is updated. This can be seen by doing this:

custom-control.component.html

{{ value }}

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

{{ i.control.value | json }} <!-- Always Updated -->
于 2020-01-13T13:48:26.620 回答