Angular ngModelChange terlambat saat memperbarui NgModel

Aug 31 2020

Saya membuat arahan menggunakan angular 8 untuk melakukan beberapa pemrosesan dan mengubah teks menjadi huruf besar. Kode yang disederhanakan di bawah ini:

html:

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

pengarahan:

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

Saat ini, tampilan diperbarui dengan benar, namun modelnya terlambat satu langkah. misalnya: jika teks input menunjukkan 'AAAA' maka ng-reflect-modelakan menampilkan 'AAAa'.

Saya telah mereproduksi kesalahan di stackblitz: Error Reproduced in Stackblitz

Tahu di mana saya salah?

Terima kasih sebelumnya!

Jawaban

2 Eliseo Aug 31 2020 at 18:40

Anda bisa mendapatkannya menggunakan

  @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
  }

Lihat bahwa kita menggunakan input @HostListener, untuk mendapatkan itemnya, bukan hanya nilainya. Ini memungkinkan kita memposisikan kursor pada posisinya setelah mengubah nilainya

CATATAN: Untuk membuat huruf besar sederhana lebih baik gunakan css text-transform: uppercase dan, ketika kita ingin mendapatkan nilainya gunakan toUpperCase ()

CATATAN2: tentang topeng lihat SO ini

9 AndreiGătej Aug 31 2020 at 19:26

TLDR

StackBlitz .

my-directive.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));
}

Jawaban rinci

Mari kita lihat mengapa itu tidak berhasil pada awalnya.

Angular memiliki pengakses nilai default untuk elemen tertentu, seperti untuk input type='text', input type='checkbox'dll ...

A ControlValueAccessoradalah perantara antara layer VIEW dan layer MODEL. Saat pengguna mengetik ke dalam input, VIEW memberitahu ControlValueAccessor, yang memiliki tugas untuk menginformasikan MODEL.

Misalnya, ketika inputperistiwa terjadi, onChangemetode ControlValueAccessorakan dipanggil. Berikut onChange tampilannya untuk setiap 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);
  });
}

Keajaiban terjadi di 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);adalah yang memanggil ngModelChangeperistiwa di custom directive. Artinya, nilai model adalah nilai dari input (dalam huruf kecil). Dan karena ControlValueAccessor.writeValue hanya menulis nilai ke VIEW, maka akan terjadi penundaan antara nilai VIEW dan nilai MODEL.

Perlu disebutkan bahwa FormControl.setValue(val)akan menulis valke kedua lapisan, VIEW dan MODEL, tetapi jika kita menggunakan ini, akan ada loop tak terbatas , karena setValue()panggilan internal viewToModelUpdate(karena MODEL harus diperbarui), dan viewToModelUpdatepanggilan setValue().

Mari kita lihat solusi yang mungkin:

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

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

Dengan pendekatan ini, Anda memodifikasi data Anda di lapisan VIEW, sebelum dikirim ke ControlValueAccessor.

Dan kita bisa yakin itu onChangeada di setiap built-in ControlValueAccessor.

Jika Anda akan membuat yang khusus, pastikan saja itu memiliki onChangeproperti. TypeScript dapat membantu Anda dengan itu.

Jika Anda ingin membaca lebih lanjut tentang bagian dalam dari @angular/forms, saya sarankan untuk melihat eksplorasi menyeluruh tentang Bentuk Sudut .

OwenKelvin Aug 31 2020 at 18:46

Dari dokumentasi

writeValue: Metode ini dipanggil oleh API formulir untuk menulis ke tampilan saat perubahan programatik dari model ke tampilan diminta.

Memanggil metode ini pada kontrol hanya akan memperbarui tampilan. Untuk memperbarui model kita dapat memanggil viewToModelUpdate (nilai: string) pada kelas NgModel. Ini akan memperbarui model

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

Di bawah ini adalah solusi yang berfungsi pada Solusi Kerja stackblitz