javascript - Problem med att markören går till slutet på ngModelChange Angular/Typescript


Jag har ett problem med mitt HTML-inputfält och TypeScript-komponenten som använder ngModelChange. Jag vill kunna redigera inmatningsvärdet varsomhelst jag behöver.

Till exempel:

  • originalinput är förifyllt med "00:00:00". Jag vill redigera det till "01:20:00".
  • med mitt tangentbord placerar jag markören (^) där jag behöver den till 0^0:00:00
  • Jag skriver in 1, resultatet är "01:00:00^"
  • Om jag vill lägga till 2 måste jag flytta markören igen, och det är inte önskvärt för mig.

Jag vet att detta är ett känt problem som skulle kunna lösas genom att återställa markören med setSelectionRange, men det har inte fungerat eftersom även om jag använde setSelectionRange(selectionStart, selectionEnd) med rätt värde på markören, skulle ngModelChange sätta tillbaka markören till slutet.

Jag har också ett Regex som sätter kolon efter varje två siffror.

Även om detta är min kod, tillhandahåller jag också en stackblitz där du kan leka med den: https://stackblitz.com/edit/angular-ivy-adynjf?file=src/app/app.compone

Detta är mitt inmatningsfält:

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

och en del av min komponent:

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);
  }
}    

I grund och botten är min fråga, hur ska denna struktur användas om vi vill ha allt: värdet ändras och formateras medan användaren skriver (vi lägger till kolon så att formatet är korrekt), och markören stannar på plats (ngModelChange ändrar inte markörens placering eller åtminstone kan jag få den att återgå till där den var)

Tack, verkligen uppskattat!!

Svar #1

Det här är inte riktigt korrekt:

även om jag använde setSelectionRange(selectionStart, selectionEnd) med det korrekta värdet för markören, skulle ngModelChange sätta tillbaka markören till slutet.

Markören placeras i slutet av inmatningsfältet av webbläsaren när värdet uppdateras via JavaScript. Inget att göra med Angular.

Låt oss titta på vad som händer när du skriver något i inmatningsfältet. Detta är en mycket väldefinierad sekvens:

  1. ngModelChange utlöses;
  2. formatAndChange körs och uppdaterar changedValue;
  3. Angulairs ändringsdetektering körs (metoden formatAndChange har slutförts vid detta tillfälle);
  4. Angular uppdaterar värdena i mallen, och uppdaterar därmed värdet som skickas till ngModel;
  5. ngModel schemalägger en mikrouppgift (jag förklarar i slutet av svaret), som uppdaterar det faktiska värdet på inmatningselementet.

Observera att när ngModel uppdateras, utlöses ngModelChange inte ens.

Om du försökte setSelectionRange inuti formatAndChange skulle det aldrig fungera, eftersom detta är vad som skulle hända:

  1. changedValue uppdateras;
  2. markören är placerad på den förväntade positionen i inmatningsfältet;
  3. ngModel och därefter uppdateras inmatningsvärdet, vilket kastar markören till slutet av inmatningen.

För att få detta att fungera behöver du anropa setSelectionRange efter att inmatningsvärdet har uppdaterats - alltså minst lika sent som en mikrouppgift efter att förändringsdetektering är klar. Här är den uppdaterade koden (notera att detta inte fungerar exakt rätt på grund av kolon mellan siffrorna, men jag är säker på att du kan lista ut det själv):

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
        );
      });
    }
  }
}

Mikrouppgifter

En mikrouppgift är i grunden någon kod som körs efter att den nuvarande anropsstacken har tömts. JavaScripts uppgifter och mikrouppgifter ligger i själva kärnan av JavaScript-motorn, och de är faktiskt inte så enkla att förstå, men väldigt användbara att förstå.

Jag vet inte varför Angular-utvecklare beslutade att uppdatera inmatningsvärdet inuti en mikrouppgift, de måste ha haft sina skäl.

Relaterade artiklar