Search code examples
angulartypescriptdata-binding

Forcing change detection when value bound to doesn't change


I have a component that reacts to a change in input control and reformats it by removing certain characters. When setting is done, the value stored in the backing field may change or not. In the first case, everything works but if the removed character maps to the previous value, there's no change detected and the component doesn't update. That leads to the value including the removable character stays in the input box.

How can I force an input box bound via [(ngModel)] to a backing field to actually update changing it's entered value to the one served by get prop() facility?

export class RowConfig {
  constructor(public amount = 0, ...) { }

  get rendition() {
    let output = "";
    if (this.amount !== null && this.amount !== undefined)
      output = ("" + this.amount)
        .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ");
    return output;
  }
  set rendition(input: string) {
    const negative = input.match(/^[-]/);
    const digits = input.replace(/\D/g, "");
    let output = null;
    if (digits)
      output = +digits * (negative ? -1 : 1);
    this.amount = output;
  }
}

The binding is done like this.

<input #amount type="text"
       (keyup)="onKeyUp($event)"
       [(ngModel)]="config.rendition">

I've tried to execute detection change in onKeyUp using markForCheck() and detectChanges() as declared in docs. No difference there.

How can I force the input box to actual clear up the current content and replace it with the actual value from the bound property?

(Playable demo on Blitzy.)


Solution

  • The trick to force the view to update even when the final value is the same as the existing one, is to first call ChangeDetectorRef.detectChanges() after setting the raw (and possibly invalid) value, and then to set the correct value.

    For example, if you had a text field that accepts only digits, and if the processing was done in the component code, you could implement the setter as follows:

    private _numericString: string;
    
    get numericString() {
      return this._numericString;
    }
    set numericString(value) {
      this._numericString = value;     // <------------------------ Set the raw value
      this.changeDetectorRef.detectChanges();   // <--------------- Trigger change detection
      this._numericString = value.replace(/[^\d]/gi, ""); // <----- Set the corrected value
    }
    

    See this stackblitz for a demo.


    In your actual code, config.Rendition is defined as a getter/setter property in a separate class, and the formatting is done in both get and set, making it more difficult to force change detection with the raw value. One way to circumvent that difficulty is to define a configRendition getter/setter property in the component and to assign that property to ngModel:

    <input #amount type="text" placeholder="xxx" [(ngModel)]="configRendition">
    

    We can then implement configRendition in such a way that ChangeDetectorRef.detectChanges() is called first with the raw value, before actually setting config.Rendition:

    private rawRendition: string;
    
    get configRendition() {
      if (this.rawRendition) {
        return this.rawRendition;
      } else {
        return this.config ? this.config.rendition : null;
      }
    }
    set configRendition(value) {
      this.rawRendition = value;
      this.detector.detectChanges();
      if (this.config) {
        this.config.rendition = value;
      }
      this.rawRendition = null;
    }
    

    See this stackblitz for a demo.