ControlValueAccessor dan contenteditable di Angular

Pernahkah Anda bertanya-tanya bagaimana kombinasi bentuk Angular dan elemen HTML di mana pengguna memasukkan data bekerja?


Sejak awal kami menggunakan ControlValueAccessor , antarmuka khusus yang hanya terdiri dari 4 metode:


 interface ControlValueAccessor { writeValue(value: any): void registerOnChange(fn: (value: any) => void): void registerOnTouched(fn: () => void): void setDisabledState(isDisabled: boolean)?: void } 

Di luar kotak, Angular memiliki beberapa pengakses seperti itu: untuk kotak centang dan tombol radio, untuk input dan seleksi. Namun, jika Anda mengembangkan obrolan di mana Anda perlu memberi kesempatan untuk menulis dalam huruf miring, membuat teks tebal atau, katakanlah, masukkan emotikon, Anda kemungkinan besar akan menggunakan atribut contenteditable untuk membuat konten yang diformat.


Angular tidak mendukung penggunaan formulir bersama dengan contenteditable , jadi Anda harus menulis sendiri.



ControlValueAccessor


Arahan yang kami tulis akan bekerja mirip dengan pengakses built-in - itu akan menanggapi atribut contenteditable . Agar templat dan formulir reaktif menerimanya melalui injeksi dependensi, cukup untuk menyediakan InjectionToken :


 @Directive({ selector: '[contenteditable][formControlName],' + '[contenteditable][formControl],' + '[contenteditable][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ContenteditableValueAccessor), multi: true, }, ], }) export class ContenteditableValueAccessor implements ControlValueAccessor { // ... } 

Antarmuka ControlValueAccessor membutuhkan 3 metode dan memiliki 1 metode opsional:


  • registerOnChange - sebuah fungsi akan masuk ke metode ini selama inisialisasi. Itu disebut dengan nilai baru ketika pengguna telah memasukkan sesuatu ke dalam komponen kami sehingga data baru dimasukkan ke dalam kontrol.
  • registerOnTouched - sebuah fungsi akan masuk ke metode ini selama inisialisasi. Itu harus dipanggil ketika pengguna meninggalkan komponen kami agar kontrol menjadi touched . Ini digunakan untuk validasi.
  • writeValue - metode ini akan dipanggil oleh kontrol untuk meneruskan nilai ke komponen kami. Ini digunakan jika nilai berubah melalui kode di luar ( setValue atau mengubah variabel yang terkait ngModel ), serta untuk mengatur nilai awal.
    Perlu dicatat bahwa, tidak seperti bentuk reaktif, ngModel berperilaku ngModel - khususnya, nilai awal di dalamnya diinisialisasi dengan penundaan, dan metode writeValue "berkedut" dua kali, pertama kali dengan null :
    https://github.com/angular/angular/issues/14988
  • setDisabledState (opsional) - metode ini akan dipanggil oleh kontrol ketika keadaan disabled berubah. Meskipun metode ini opsional, lebih baik untuk merespons ini di komponen Anda.

Implementasi antarmuka


Untuk bekerja dengan elemen DOM, kita perlu Renderer2 dan, pada kenyataannya, elemen itu sendiri, jadi tambahkan mereka ke konstruktor:


 constructor( @Inject(ElementRef) private readonly elementRef: ElementRef, @Inject(Renderer2) private readonly renderer: Renderer2, ) {} 

Kami menyimpan metode yang akan diberikan kontrol kepada kami di bidang pribadi kelas:


 private onTouched = () => {}; private onChange: (value: string) => void = () => {}; registerOnChange(onChange: (value: string) => void) { this.onChange = onChange; } registerOnTouched(onTouched: () => void) { this.onTouched = onTouched; } 

keadaan disabled untuk komponen contenteditable setara dengan menonaktifkan mode edit - contenteditable="false" . Menyetel nilai dari luar sama dengan mengganti DOM innerHTML elemen, dan memperbarui nilai oleh pengguna dan meninggalkan komponen dapat dipantau dengan berlangganan ke acara yang sesuai:


 @HostListener('input') onInput() { this.onChange(this.elementRef.nativeElement.innerHTML); } @HostListener('blur') onBlur() { this.onTouched(); } setDisabledState(disabled: boolean) { this.renderer.setAttribute( this.elementRef.nativeElement, 'contenteditable', String(!disabled), ); } writeValue(value: string) { this.renderer.setProperty( this.elementRef.nativeElement, 'innerHTML', value, ); } 

Faktanya, itu saja. Ini cukup untuk implementasi dasar pekerjaan bentuk Angular dan elemen yang dapat contenteditable .


Penjelajah internet


Namun, ada beberapa tapi.


Pertama, nilai awal kosong dari formulir adalah null , dan setelah writeValue di writeValue , kita akan melihat null di templat. Untuk mengimplementasikan pekerjaan dengan benar, kita perlu menormalkan nilai:


 writeValue(value: string | null) { this.renderer.setProperty( this.elementRef.nativeElement, 'innerHTML', ContenteditableValueAccessor.processValue(value), ); } private static processValue(value: string | null): string { const processed = value || ''; return processed.trim() === '<br>' ? '' : processed; } 

Di sini kita juga akan menangani situasi berikut. Bayangkan bahwa konten suatu elemen memiliki tag HTML. Jika kita hanya memilih semuanya dan menghapusnya, itu tidak akan kosong di dalamnya - tag <br> mandiri akan dimasukkan di sana. Agar tidak menyumbat kontrol dengan nilai kosong, kami akan menganggapnya sebagai string kosong.


Kedua, Internet Explorer tidak mendukung acara input untuk elemen yang dapat contenteditable . Kami harus menerapkan fallback menggunakan MutationObserver :


 private readonly observer = new MutationObserver(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); ngAfterViewInit() { this.observer.observe(this.elementRef.nativeElement, { characterData: true, childList: true, subtree: true, }); } ngOnDestroy() { this.observer.disconnect(); } 

Kami tidak akan menerapkan verifikasi pada browser tertentu. Sebagai gantinya, kami segera menonaktifkan MutationObserver pada acara input pertama:


 @HostListener('input') onInput() { this.observer.disconnect(); // ... } 

Sekarang komponen kami berfungsi di IE11 dan kami puas dengan diri kami sendiri!


Internet Explorer! αƒš (ΰ²  η›Š ΰ²  αƒš)


Sayangnya, IE11 tidak akan ketinggalan. Rupanya, ada bug dalam karya MutationObserver . Jika ada tag di dalam elemen contenteditable , misalnya, some <b>text</b> , maka ketika menghapus teks, yang akan berarti menghapus seluruh tag ( text kata dalam contoh ini), panggilan balik pengamat akan dipanggil sebelum real perubahan DOM!



Sayangnya, kami tidak punya pilihan selain mengakui kekalahan dan menggunakan setTimeout :


 private readonly observer = new MutationObserver(() => { setTimeout(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); }); 

Kesimpulan


Asalkan Angular harus mendukung Internet Explorer versi 9, 10 dan 11, menjadi jelas mengapa mereka tidak mengimplementasikan pekerjaan dengan contenteditable di rumah.


Selain itu, Anda harus ingat bahwa HTML dapat berisi kode berbahaya - karena itu, jangan dengan berani mengambil konten yang tidak dikenal dan menempelkannya ke dalam kontrol, dan input pengguna harus dicek dalam acara paste dan drop . Kode yang dijelaskan dalam artikel ini berfungsi di Angular 4 dan lebih tinggi, termasuk FormHooks . Jika mau, Anda dapat menambahkan dukungan untuk Angular 2, jika Anda menggunakan Renderer , bukan Renderer2 . Kode sumber dan paket npm tersedia di tautan:


https://github.com/TinkoffCreditSystems/angular-contenteditable-accessor
https://www.npmjs.com/package/@tinkoff/angular-contenteditable-accessor


Dan mainkan dengan contoh di sini:
https://stackblitz.com/edit/angular2-contenteditable-value-accessor

Source: https://habr.com/ru/post/id443714/


All Articles