简体   繁体   English

cursor 问题在 ngModelChange Angular/Typescript 上结束

[英]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.我的 HTML 输入字段和使用 ngModelChange 的 typescript 组件有问题。 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".原始输入自动填充为“00:00:00”。 I want to edit it to "01:20:00".我想将其编辑为“01:20:00”。
  • with my keyboard I position the cursor (^) where I need it to 0^0:00:00用我的键盘我 position cursor (^) 我需要它到 0^0:00:00
  • I type in 1, the result is "01:00:00^"我输入 1,结果是“01:00:00^”
  • If I want to add the 2, I need to move the cursor again, and that for me is not desirable.如果我想添加 2,我需要再次移动 cursor,这对我来说是不可取的。

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.我知道这是一个已知问题,可以通过使用 setSelectionRange 重新设置 cursor 来解决,但是这没有用,因为即使我使用 setSelectionRange(selectionStart, selectionEnd) 和 cursor 的正确值,ngModelChange 也会把cursor 回到最后。

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虽然这是我的代码,但我还提供了一个 stackblitz,您可以在其中使用它: 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)基本上我的问题是,如果我们想要它,应该如何使用这个结构:值在用户键入时更改并格式化(我们添加冒号以便格式正确),并且 cursor 保持不变(ngModelChange不会改变 cursor 的位置,或者至少我可以让它回到原来的位置)

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.即使我使用 setSelectionRange(selectionStart, selectionEnd) 和 cursor 的正确值,ngModelChange 也会将 cursor 放回末尾。

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.每当值通过 JavaScript 更新时,浏览器会将 cursor 放在输入字段的末尾。与 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; ngModelChange触发;
  2. formatAndChange runs and updates changedValue ; formatAndChange运行并更新changedValue
  3. Angular's change detection runs (the formatAndChange method has completed by this point); Angular 的变更检测运行(此时formatAndChange方法已经完成);
  4. Angular updates the values in the template, thus updating the value passed to ngModel ; Angular 更新模板中的值,从而更新传递给ngModel的值;
  5. ngModel schedules a microtask (I'll explain at the end of the answer), which updates the actual input element value. ngModel安排了一个微任务(我会在答案的最后解释),它会更新实际的输入元素值。

Note that when ngModel is updated, ngModelChange is not even fired.请注意,当ngModel更新时,甚至不会触发ngModelChange

If you were trying to setSelectionRange inside formatAndChange , it was never going to work, because this is what would happen:如果您尝试在setSelectionRangeformatAndChange ,它永远不会起作用,因为这是会发生的事情:

  1. changedValue is updated; changedValue被更新;
  2. the cursor is placed at the expected position in the input field; cursor 被放置在输入字段中预期的 position 处;
  3. ngModel and subsequently the input value are updated, throwing the cursor to the end of the input. ngModel和随后的输入值被更新,将 cursor 扔到输入的末尾。

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.为了使这个工作正常,您需要在输入值更新调用setSelectionRange - 所以至少在更改检测完成后调用微任务。 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. Javascript 的任务和微任务是 JavaScript 引擎的核心,它们实际上并不那么容易掌握,但理解起来非常有用。

I do not know why Angular developers decided to update the input value inside a microtask, must've had their reasons.我不知道为什么 Angular 开发人员决定更新微任务中的输入值,一定有他们的原因。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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