This is simple custom form control
@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;
}
}
Used like below:
@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;
}
}
What I fail to understand is why ngModel of the control is only being updated from changing value of the outer input, but not in the case of using inner input? Live example here: https://stackblitz.com/edit/angular-7apjhg
Edit:
Actual problem can be seen with simpler example (without inner input):
@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;
}
}
After clicking the button inside the custom control, property value on parent is updated, but ngModel is not. Updated example: https://stackblitz.com/edit/angular-tss2f3
In order for this to work, you'll have to use the banana in the box syntax for the input that resides inside custom-control.component.ts
<input [(ngModel)]="value" (ngModelChange)="onChange($event)">
That happens because when you are typing into the outer input, the CustomControlComponent
's ControlValueAccessor.writeValue()
will be executed, which in turn will update the inner input.
Let's break it into smaller steps.
1) type in the outer input
2) change detection is triggered
3) the ngOnChanges
from NgModel
directive(that is bound to custom-control
) will eventually reached, which will cause the FormControl
instance to be updated in the next tick
@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()
will invoke the registered change function callback, which will in turn invoke ControlValueAccessor.writeValue
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor !.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
Where dir.valueAccessor !.writeValue(newValue)
will be the CustomControlComponent.writeValue
function.
writeValue(value: any) {
this.value = value;
}
This is why you're inner input is being updated by the outer one.
Now, why doesn't it work the other way around?
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:
{{ value }}
<input #i="ngModel" [ngModel]="value" (ngModelChange)="onChange($event)">
{{ i.control.value | json }} <!-- Always Updated -->