I have an input field where the user can enter the rate of something.
When the user enters a value, I want it to be displayed after rounding it off and then the updated value to be stored on the backing model in ts
file.
Using an Angular pipe isn't good for this since a pipe in one directional and the updated value won't be reflected on the model.
So to make it bidirectional, I'm doing the following:
<input type="text" [ngModel]="model.rate" (ngModelChange)="model.rate=roundRate($event)" name="rate" />
The roundDate
function looks like this
roundRate(value) {
return Math.round(value);
}
Now when I enter values like 5.6, 7.8, 9.5 etc they're all rounded off, displayed and stored on the model to 6, 8 and 10 respectively, which is the expected behavior.
The problem starts when I enter 2.1, 3.4, 5.3 etc. In this case, the roundRate
function gets called and it returns the value after rounding off. But the values shown on screen are still the old values (2.1, 3.4, 5.3)
I inspected the input
element on Chrome and found that the ng-reflect-model
property was getting updated to the expected values (2, 3, 5).
<input _ngcontent-c39="" name="rate" type="text" ng-reflect-name="rate" ng-reflect-model="5" class="ng-valid ng-dirty ng-touched">
Can someone please explain what is happening here and despite the ng-reflect-model
property getting updated why the screen still shows the old values?
Thanks for your help!
Begin with a closer look at the ngModel
directive API, you will see that ngModel
is @Input
binding which accepts some value as model
variable. At the time of initializing ngModel
control it creates FormControl
implicitly.
public readonly control: FormControl = new FormControl();
The magic of updating model
value happens from ngOnChanges
hook, this way it syncs the view value with the model
value. If you take a closer look at the ngOnChanges
hook, you will find that it validates the input and applies other checks as well, afterwards it strictly checks if the value of ngModel
has really changed using the isPropertyUpdated
method.
ngOnChanges - ngModel
directive
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); // helps to update
this.viewModel = this.model;
}
}
private _updateValue(value: any): void {
resolvedPromise.then(() => {
// set value will set form control
this.control.setValue(value, {emitViewToModelChange: false});
});
}
But, to make it happen, Angular should recognize the changes during change detection cycle. And, since we haven't changed our model, it won't trigger the ngOnChanges hook:
What I explained till now was the API part. Lets come back to the question.
Try the below snippet in stackblitz, what our expectation would be. On input value change, it should set that value to 10
itself.
<input type="text"
[ngModel]="model.rate"
(ngModelChange)="model.rate = 10"
name="rate" />
Unfortunately, it doesn't happen in that way, you will see that on initially typing any number, it will change the input value to 10
and later whatever you type will keep on appending to the number input. Ahh, wondering why?
Let's go back again to the original question,
<input type="text"
[ngModel]="model.rate"
(ngModelChange)="model.rate=roundRate($event)"
name="rate" />
{{model.rate}}
ngModel
is used as a one way binding. Expected behavior is, whatever values assigned to the model.rate
should be reflected inside the input
. Let's try to enter 1.1, you will see that it shows us the value 1.1
. Now try to enter 1.2
, this results in 1
. Wondering why? But certainly model.rate
bindings update correctly.
Similarly Check for 4.6
and 4.8
. They result in 5, which works perfect.
Let's break down the above example, what happens when you try to enter 1.1
.
1
=> model.rate becomes 1
1.
=> model.rate stays 1
1.1
=> model.rate stays 1
Eventually when you type 1.1
or 1.2
the model value stays 1
since we Math.round
the value. Since it isn't updating the model.rate
value, whatever you enter further is just ignored by the ngModel
binding, and is shown inside the input
field.
Checking for 4.6
, 4.8
works perfectly. Why? break it down step wise
4
=> model.rate becomes 4
4.
=> model.rate stays 4
4.6
=> model.rate becomes 5
Over here, when you enter 4.6
in textbox, the Math.round
value becomes 5
. which is a change in the model.rate
(ngModel) value and would result in an update in the input
field value.
Now start reading the answer again, the technical aspect explained initially should be clear as well.
Note: While reading an answer follow the links provided to snippet, it may help you understand it more precisely.
To make it working you can try updating your fields on blur
/change
where this event fires on the focus out of fields
like Sid's answer
. It works because you're updating the model once when the field is focused out.
But it works only once. To keep updating constantly we can do a trick like this:
this.model.rate = new String(Math.round(value));
which will result in a new object reference each time we round our value.