简体   繁体   中英

Issue with cursor going to the end on ngModelChange Angular/Typescript

I'm having an issue with my HTML input field and the typescript component that is using ngModelChange. I want to be able to edit the input value wherever I need to.

For example:

  • original input is auto-filled with is "00:00:00". I want to edit it to "01:20:00".
  • with my keyboard I position the cursor (^) where I need it to 0^0:00:00
  • I type in 1, the result is "01:00:00^"
  • If I want to add the 2, I need to move the cursor again, and that for me is not desirable.

I know this is a known issue that could be fixed with re-setting the cursor using setSelectionRange, however that has not worked since even if I used the setSelectionRange(selectionStart, selectionEnd) with the correct value of the cursor, the ngModelChange, would put the cursor back to the end.

I also have a Regex that applies the colon after each two digits.

Although this is my code, I also provide a stackblitz where you can play with it: https://stackblitz.com/edit/angular-ivy-adynjf?file=src/app/app.compone

This is my input field:

<input
  id="value"
  type="text"
  [ngModel]="changedValue"
  (ngModelChange)="formatAndChange($event)"
/>

and part of my component:

export class AppComponent {
  public changedValue: String = "00:00:00";

  public formatAndChange(inputValue: string) {
    this.changedValue = inputValue;

    if (inputValue.length > 8) {
      inputValue = inputValue.substr(0, 8);
    }
    let unformat = inputValue.replace(/\D/g, "");
    if (unformat.length > 0) {
      inputValue = unformat.match(new RegExp(".{1,2}", "g")).join(":");
    }

    this.changedValue = new String(inputValue);
  }
}    

Basically my question is, how is this structure supposed to be used if we want it all: the value changes and is formatted while the user is typing (we add the colon so the format is correct), and the cursor stays in place (ngModelChange does not change the cursor placement or at least I can make it return to where it was)

Appreciate it. Thanks!!

This is not quite correct:

even if I used the setSelectionRange(selectionStart, selectionEnd) with the correct value of the cursor, the ngModelChange, would put the cursor back to the end.

The cursor is placed at the end of the input field by the browser whenever the value is updated via JavaScript. Nothing to do with Angular.

Let's take a look at what happens when you type something in the input field. This is a very well-defined sequence:

  1. ngModelChange fires;
  2. formatAndChange runs and updates changedValue ;
  3. Angular's change detection runs (the formatAndChange method has completed by this point);
  4. Angular updates the values in the template, thus updating the value passed to ngModel ;
  5. ngModel schedules a microtask (I'll explain at the end of the answer), which updates the actual input element value.

Note that when ngModel is updated, ngModelChange is not even fired.

If you were trying to setSelectionRange inside formatAndChange , it was never going to work, because this is what would happen:

  1. changedValue is updated;
  2. the cursor is placed at the expected position in the input field;
  3. ngModel and subsequently the input value are updated, throwing the cursor to the end of the input.

To get this working you need to call setSelectionRange after the input value is updated - so at least as late as a microtask after change detection is complete. Here's the updated code (note that this does not work exactly right due to colons between the digits, but I am sure you can figure that out by yourself):

import {
  AfterViewChecked,
  Component,
  ElementRef,
  ViewChild
} from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewChecked {
  public changedValue: String = '00:00:00';

  private valueUpdated: boolean;

  private selectionStart: number;

  private selectionEnd: number;

  private selectionDirection: 'forward' | 'backward' | 'none';

  @ViewChild('input')
  private inputRef: ElementRef<HTMLInputElement>;

  public formatAndChange(inputValue: string) {
    console.log(inputValue);
    const oldChangedValue = this.changedValue;
    this.changedValue = inputValue;

    if (inputValue.length > 8) {
      inputValue = inputValue.substr(0, 8);
    }
    let unformat = inputValue.replace(/\D/g, '');
    if (unformat.length > 0) {
      inputValue = unformat.match(new RegExp('.{1,2}', 'g')).join(':');
    }
    console.log(inputValue);

    this.changedValue = new String(inputValue);

    this.valueUpdated = oldChangedValue !== this.changedValue;

    if (this.valueUpdated && this.inputRef.nativeElement) {
      const element = this.inputRef.nativeElement;
      this.selectionStart = element.selectionStart;
      this.selectionEnd = element.selectionEnd;
      this.selectionDirection = element.selectionDirection;
    }
  }

  // This lifecycle hook is called after change detection is complete for this component
  ngAfterViewChecked() {
    // This method is called VERY often, so we need to make sure that we only execute this logic when truly necessary (i.e. the value has actually changed)
    if (this.valueUpdated && this.inputRef.nativeElement) {
      this.valueUpdated = false;

      // This is how you schedule a microtask
      Promise.resolve().then(() => {
        // Make sure you update this to deal with colons
        this.inputRef.nativeElement.setSelectionRange(
          this.selectionStart,
          this.selectionEnd,
          this.selectionDirection
        );
      });
    }
  }
}

Microtasks

A microtask is basically some code, that is executed after the current call stack empties. Javascript's tasks and microtasks are at the very core of the JavaScript engine, and they are actually not that simple to grasp, but very useful to understand.

I do not know why Angular developers decided to update the input value inside a microtask, must've had their reasons.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM