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)">
发生这种情况是因为当您在外部输入中输入时,CustomControlComponent
的 ControlValueAccessor.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 -->
这是简单的自定义表单控件
@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
自定义-control.component.ts
<input [(ngModel)]="value" (ngModelChange)="onChange($event)">
发生这种情况是因为当您在外部输入中输入时,CustomControlComponent
的 ControlValueAccessor.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 -->