Angular ngModelChange опаздывает при обновлении NgModel

Aug 31 2020

Я делаю директиву, используя angular 8, чтобы выполнить некоторую обработку и преобразовать текст в верхний регистр. Упрощенный код ниже:

html:

<input class="form-control" id="label" name="label" required myDirective>

директива:

import { Directive, HostListener } from '@angular/core';
import { NgControl } from '@angular/forms';

@Directive({
  selector: '[myDirective]'
})
export class Mydirective {
  constructor(private control: NgControl) { }

  processInput(value: any) {
     // do some formatting
     return value.toUpperCase();
  }

  @HostListener('ngModelChange', ['$event'])
  ngModelChange(value: any) {
     this.control.valueAccessor.writeValue(this.processInput(value));
  }
}

Сейчас вид обновляется корректно, но модель опаздывает на один шаг. например: если во вводимом тексте отображается «AAAA», то ng-reflect-modelбудет отображаться «AAAa».

Я воспроизвел ошибку в stackblitz: Ошибка воспроизведена в Stackblitz

Есть идеи, где я ошибаюсь?

Спасибо раньше!

Ответы

2 Eliseo Aug 31 2020 at 18:40

вы можете получить это используя

  @HostListener('input', ['$event'])
  ngModelChange(event: any) {
    const item = event.target
    const value = item.value;
    const pos = item.selectionStart;
    this.control.control.setValue(this.processInput(value), { emit: false });
    item.selectionStart = item.selectionEnd = pos
  }

Обратите внимание, что мы используем ввод @HostListener, чтобы получить элемент, а не только значение. Это позволяет нам позиционировать курсор в его позиции после изменения значения

ПРИМЕЧАНИЕ: Чтобы сделать простой верхний регистр, лучше использовать css text-transform: uppercase и, когда мы хотим получить значение, используйте toUpperCase ()

ПРИМЕЧАНИЕ 2: о маске см. Это ТАК

9 AndreiGătej Aug 31 2020 at 19:26

TL; DR

StackBlitz .

моя директива.directive.ts

/* ... */

ngOnInit () {
  const initialOnChange = (this.ngControl.valueAccessor as any).onChange;

  (this.ngControl.valueAccessor as any).onChange = (value) => initialOnChange(this.processInput(value));
}

/* ... */

@HostListener('ngModelChange', ['$event'])
ngModelChange(value: any) {
  this.ngControl.valueAccessor.writeValue(this.processInput(value));
}

Подробный ответ

Посмотрим, почему изначально это не сработало.

Angular имеет средства доступа к значениям по умолчанию для определенных элементов, таких как for input type='text'и input type='checkbox'т. Д.

A ControlValueAccessorявляется посредником между слоями VIEW и MODEL. Когда пользователь вводит ввод, ВИД уведомляет об этом ControlValueAccessor, которому поручено проинформировать МОДЕЛЬ.

Например, когда inputпроисходит событие, будет вызван onChangeметод ControlValueAccessor. Вот как onChange выглядит каждый ControlValueAccessor :

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
  dir.valueAccessor!.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingChange = true;
    control._pendingDirty = true;

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

Волшебство происходит в updateControl:

function updateControl(control: FormControl, dir: NgControl): void {
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
 
  // !
  dir.viewToModelUpdate(control._pendingValue);
  control._pendingChange = false;
}

dir.viewToModelUpdate(control._pendingValue);это то, что вызывает ngModelChangeсобытие в пользовательской директиве. Это означает, что значение модели - это значение из ввода (в нижнем регистре). И поскольку записывает ControlValueAccessor.writeValue только значение в VIEW, будет задержка между значением VIEW и значением MODEL.

Стоит упомянуть, что FormControl.setValue(val)запись будет valвыполняться в оба уровня, ВИД и МОДЕЛЬ, но если бы мы использовали это, был бы бесконечный цикл , так как setValue()внутренние вызовы viewToModelUpdate(потому что МОДЕЛЬ должна быть обновлена) и viewToModelUpdateвызовы setValue().

Давайте посмотрим на возможное решение:

ngOnInit () {
  const initialOnChange = (this.ngControl.valueAccessor as any).onChange;

  (this.ngControl.valueAccessor as any).onChange = (value) => initialOnChange(this.processInput(value));
}

При таком подходе вы изменяете свои данные на уровне VIEW, прежде чем они будут отправлены в ControlValueAccessor.

И мы можем быть уверены, что он onChangeесть на каждом встроенном ControlValueAccessor.

Если вы собираетесь создать собственный, просто убедитесь, что у него есть onChangeсвойство. TypeScript может вам в этом помочь.

Если вы хотите узнать больше о внутреннем устройстве @angular/forms, я бы порекомендовал взглянуть на подробное исследование Angular Forms .

OwenKelvin Aug 31 2020 at 18:46

Из документации

writeValue: этот метод вызывается API форм для записи в представление, когда запрашиваются программные изменения от модели к представлению.

Следовательно, вызов этого метода для элемента управления приведет только к обновлению представления. Чтобы обновить модель, мы можем вызвать viewToModelUpdate (value: string) в классе NgModel. Это обновит модель

constructor(private model: NgModel) { }
processInput(value: any) {
   // do some formatting
   return value.toUpperCase();
}

@HostListener('input', ['$event'])
ngModelChange(input: any) {
  const value = input.target.value
  this.model.valueAccessor.writeValue(this.processInput(value));
  this.model.viewToModelUpdate(this.processInput(value));
}

Ниже приведен рабочий раствор на stackblitz рабочего раствора