9

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!

Jnr
  • 1,504
  • 2
  • 21
  • 36
Yogesh
  • 699
  • 1
  • 6
  • 21

4 Answers4

13

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:

enter image description here

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. type 1 => model.rate becomes 1
  2. type 1. => model.rate stays 1
  3. type 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

  1. type 4 => model.rate becomes 4
  2. type 4. => model.rate stays 4
  3. type 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.

Snippet in Stackblitz

Aaron Ford
  • 82
  • 7
Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • Thanks Suresh, glad to know it helped ;) – Pankaj Parkar Nov 23 '18 at 05:01
  • Perfect. This is what I was looking for. Thanks Panjak. – SiddAjmera Nov 23 '18 at 05:07
  • 1
    I would say that the main reason here is how Angular change detection mechanism works under the hood. It won't update property or trigger ngOnChanges hook if value hasn't changed. We can see it here https://stackblitz.com/edit/angular-ngmodelchanges-eg-nccjh1?file=src%2Fapp%2Fapp.component.html – yurzui Nov 23 '18 at 05:07
  • 1
    Also blur/change event will help only once. If we type 1.2 then click outside the input value will become 1, after that if we try to change the value from 1 to 1.2 and trigger blur that value won't be changed to 1. https://stackblitz.com/edit/angular-ngmodelchanges-eg-jvvzrs?file=src/app/app.component.ts – yurzui Nov 23 '18 at 05:10
  • 2
    But if we will change the value like `this.model.rate = new String(Math.round(value));` then `change/blur` events will definitely work https://stackblitz.com/edit/angular-ngmodelchanges-eg-upzz5p?file=src/app/app.component.ts – yurzui Nov 23 '18 at 05:15
  • 1
    @PankajParkar Now I got to know why that didn't work. @yurzui Yep, the ``(change)` event also worked only once. – Yogesh Nov 23 '18 at 05:17
  • Yep, that's exactly what I wanted to say @yurzui . Feel free to edit answer :) – Pankaj Parkar Nov 23 '18 at 05:22
  • awesome explanation @PankajParkar – NeverGiveUp161 Feb 19 '20 at 12:32
  • didn't read the whole answer because wtf angular but the solution works. Why does anyone use this framework? – Felipe Jul 14 '21 at 21:40
4

For a cleaner implementation, just use the (change) @Output property and [(ngModel)]. The implementation of roundRate will change something like this:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  model = { rate: null };

  roundRate() {
    this.model.rate = Math.round(+this.model.rate);
  }
}

And in template:

<input type="text" [(ngModel)]="model.rate" (change)="roundRate()" name="rate" />

PS: This will update the value only once you blur from your input field


Here's a Sample StackBlitz for your ref.

SiddAjmera
  • 38,129
  • 5
  • 72
  • 110
1

In Angular change detection strategy help to reflect changes on UI.

Please use below code:

Step 1: Import ChangeDetectorRef in your component

import { ChangeDetectorRef} from angular/core';

Step 2: Create instance of ChangeDetectorRef on constructor.

constructor(private ref: ChangeDetectorRef){
}

Step 3: Call where you are updating value.

this.ref.detectChanges();
sanjay kumar
  • 838
  • 6
  • 10
1

We can also obtain its value using $event and not using ngModel-

component.html

<table>
<tbody>
<tr>
<td colspan="2" style="width:100px;height:38px;"><input type = "text"  (change)="enterDirectRowNumberResponse(data,$event.target.value)"></td></tr>
</tbody
</table>

component.ts

enterDirectRowNumberResponse(data,event){
        console.log ("Inside enterDirectRowNumberResponse",event)
        console.log ("data = ", data );
}
Techdive
  • 997
  • 3
  • 24
  • 49