Search code examples
javascriptangulartypescript

Forcing input value (Angular)


I have the following input

<input type="number" [ngModel]="inputValue" (ngModelChange)="handleChange($event)"/>

And I am trying to force the input to stay less or equal to 100. using the method handleChange

export class AppComponent {

  inputValue: number = 0;


  handleChange($event: any) {
    if ($event > 100) {
      this.inputValue = 100;
    }
  }
}

And it only works the first time I enter the first input that is higher than 100, but after that it doesn't.

My understanding of it is that the DOM doesn't update when there are no value updates.

I can solve this problem using other methods like @ViewChild for example, but I am more interested in knowing how this works and how angular handles this specific use case.

Thanks!


Solution

  • The solution is to introduce two-way binding, so your property and the html value are synchronized. You also need to force change detection before setting the property to 100.

    Stackblitz: https://stackblitz.com/edit/angular-ivy-upykps?file=src/app/app.component.ts

    Both of these steps are necessary for the change detector to detect that the property has changed every time, only then will it update the html value with your property value.

    Two way binding is accomplished with the banana-in-a-box [(🍌)] notation.

    Change detection is forced with the ChangeDetectorRef injectable service.

    <input type="number" [(ngModel)]="inputValue" (ngModelChange)="handleChange($event)"/>
    
      inputValue: number = 0;
    
      constructor(private cd: ChangeDetectorRef){};
    
      handleChange($event: any) {
        this.cd.detectChanges();
        if ($event > 100) {
          this.inputValue = 100;
        }
      }
    

    I know this is a bit unintuitive but it has to do with how the change detector detects changes. If you really want to understand it, I suggest adding a debugger line at the start of your function and walking through the steps.

    I'll explain what was happening the way you had it set up:

    The html value is set to 0 via data binding and the change detector notes that the property is currently 0. On every click event, change detection is run after the ngModelChange callback. So, the first time you set the property to 100, the change detector sees - oh yes, the property used to be 0, now it is 100, a change has occurred - it sets a flag that causes the html to be updated with the new value, and notes that the property is currently 100.

    The problem is you never changed inputValue away from 100 - so every time you set it to 100 - the change detector sees no change. It will never update the html again.

    Using two-way binding, the property is updated whenever the html value is changed - this happens before ngModelChange. But this does not update the change detector's state! It will work when we go from less than 100 to greater than 100, but if we continue passing more numbers that are larger than 100, it won't be reset. That's because change detection is run after ngModelChange, and we actually overwrite the larger value to 100 before the change detector can see the larger value. We get the same problem - the change detector sees 100 every time and does not attempt to update the html.

    You need the forced change detection so the change detector can note down that the property changed to this larger value. Then after setting the property to 100, it will see - ah yes, the property used to be this larger value, now it is 100 - it sets the "changed" flag which triggers a DOM update.

    An example when the user changes the html from 100 to 101:

    The change detector notes the current state on every change detection, but NOT during data binding. DOM updates are only triggered when the change detector finds a change.

    Initial State
    ACTUAL
    html value: 100
    property: 100
    CHANGE DETECTOR
    property: 100
    
    User changes html value to 101
    ACTUAL
    html value: 101
    property: 100
    CHANGE DETECTOR
    property: 100
    
    Round bracket data binding updates property
    ACTUAL
    html value: 101
    property: 101
    CHANGE DETECTOR
    property: 100
    
    handleChange calls ChangeDetectorRef.detectChanges()
    ACTUAL
    html value: 101
    property: 101
    CHANGE DETECTOR
    property: 100 - CHANGED!
    **This is the important part 
    the change detector sees that the property changed to 101**
    
    Square bracket data binding attempts to update html because of a change
    It's already correct though
    ACTUAL
    html value: 101
    property: 101
    CHANGE DETECTOR
    property: 101
    
    handleChange changes property to 100
    ACTUAL
    html value: 101
    property: 100
    CHANGE DETECTOR
    property: 101
    
    change detector detects changes because click event occurred
    ACTUAL
    html value: 101
    property: 100
    CHANGE DETECTOR
    property: 101 - CHANGED!
    **If this was still at 100, no updates would be made. 
    There would be a mismatch between property and html value**
    
    Square bracket data binding updates html because of a change
    ACTUAL
    html value: 100
    property: 100
    CHANGE DETECTOR
    property: 100