ControlValueAccessor und contenteditable in Angular

Haben Sie sich jemals gefragt, wie die Kombination von Winkelformularen und HTML-Elementen, über die der Benutzer Daten eingibt, funktioniert?


Von Anfang an haben wir ControlValueAccessor , eine spezielle Schnittstelle, die nur aus 4 Methoden besteht:


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

Angular verfügt standardmäßig über mehrere solcher Accessoren: für Kontrollkästchen und Optionsfelder, für die Eingabe und Auswahl. Wenn Sie jedoch einen Chat entwickeln, in dem Sie die Möglichkeit geben müssen, kursiv zu schreiben, den Text fett zu gestalten oder beispielsweise Emoticons einzufügen, verwenden Sie höchstwahrscheinlich das Attribut contenteditable , um formatierten Inhalt zu erstellen.


Angular unterstützt die Verwendung von Formularen zusammen mit contenteditable , daher müssen Sie diese selbst schreiben.



ControlValueAccessor


Die Direktive, die wir schreiben, funktioniert ähnlich wie die integrierten Accessoren - sie reagiert auf das Attribut contenteditable . Damit Vorlagen und reaktive Formulare sie durch Abhängigkeitsinjektion empfangen können, reicht es aus, ein integriertes InjectionToken bereitzustellen:


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

Die ControlValueAccessor Schnittstelle erfordert 3 Methoden und verfügt über 1 optionale Methode:


  • registerOnChange - Während der Initialisierung wird eine Funktion in diese Methode aufgenommen. Es wird mit einem neuen Wert aufgerufen, wenn der Benutzer etwas in unsere Komponente eingegeben hat, damit neue Daten in das Steuerelement eingegeben werden.
  • registerOnTouched - Während der Initialisierung wird eine Funktion in diese Methode aufgenommen. Es muss aufgerufen werden, wenn der Benutzer unsere Komponente verlassen hat, damit das Steuerelement touched . Dies wird zur Validierung verwendet.
  • writeValue - Diese Methode wird vom Steuerelement aufgerufen, um den Wert an unsere Komponente zu übergeben. Es wird verwendet, wenn sich der Wert durch den Code außerhalb ändert ( setValue oder Ändern der Variablen, an die ngModel gebunden ist), sowie zum Festlegen des Anfangswertes.
    Es ist anzumerken, dass sich ngModel im Gegensatz zu reaktiven Formen ngModel verhält - insbesondere wird der Anfangswert darin mit einer Verzögerung initialisiert, und die writeValue Methode "zuckt" zweimal, das erste Mal mit null :
    https://github.com/angular/angular/issues/14988
  • setDisabledState (optional) - Diese Methode wird vom Steuerelement aufgerufen, wenn sich der disabled Status ändert. Obwohl die Methode optional ist, ist es besser, in Ihrer Komponente darauf zu reagieren.

Schnittstellenimplementierung


Um mit dem DOM-Element arbeiten zu können, benötigen wir Renderer2 und das Element selbst. Fügen Sie sie also dem Konstruktor hinzu:


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

Wir speichern die Methoden, die die Steuerung an uns weitergibt, in den privaten Feldern der Klasse:


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

disabled Status für die contenteditable Komponente entspricht dem Deaktivieren des Bearbeitungsmodus - contenteditable="false" . Das Festlegen des Werts von außen entspricht dem Ersetzen des innerHTML DOM des Elements. Das Aktualisieren des Werts durch den Benutzer und das Verlassen der Komponente kann durch Abonnieren der entsprechenden Ereignisse überwacht werden:


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

Das ist in der Tat alles. Dies reicht für die grundlegende Implementierung der Arbeit von Angular-Formen und contenteditable Elementen aus.


Internet Explorer


Es gibt jedoch ein paar aber.


Erstens ist der leere Anfangswert des Formulars null , und nach dem writeValue in IE11 wird in der Vorlage null angezeigt. Um die Arbeit korrekt umzusetzen, müssen wir den Wert normalisieren:


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

Hier werden wir auch die folgende Situation behandeln. Stellen Sie sich vor, der Inhalt eines Elements hätte HTML-Tags. Wenn wir einfach alles auswählen und löschen, wird es im Inneren nicht leer sein - dort wird ein einzelnes <br> -Tag eingefügt. Um das Steuerelement nicht mit einem leeren Wert zu verstopfen, wird es als leere Zeichenfolge betrachtet.


Zweitens unterstützt Internet Explorer keine input für contenteditable Elemente. Wir müssen Fallback mit MutationObserver implementieren:


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

Wir werden keine Überprüfung in einem bestimmten Browser implementieren. Stattdessen deaktivieren wir MutationObserver sofort beim ersten input :


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

Jetzt funktioniert unsere Komponente in IE11 und wir sind mit uns zufrieden!


Internet Explorer! ლ (ಠ 益 ಠ ლ)


Leider wird IE11 nicht zurückgelassen. Anscheinend gibt es einen Fehler in der Arbeit von MutationObserver . Wenn das contenteditable Element Tags enthält, z. B. some <b>text</b> , wird beim Löschen von Text, bei dem das gesamte Tag (in diesem Beispiel der Worttext) gelöscht wird, der Rückruf des Beobachters vor dem Real aufgerufen Änderungen im DOM!



Leider haben wir keine andere Wahl, als eine Niederlage zuzugeben und setTimeout :


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

Fazit


Vorausgesetzt, Angular sollte die Internet Explorer-Versionen 9, 10 und 11 unterstützen, wird klar, warum sie zu Hause keine Arbeit mit contenteditable implementiert haben.


Darüber hinaus müssen Sie bedenken, dass HTML schädlichen Code enthalten kann. Nehmen Sie daher nicht mutig unbekannten Inhalt und fügen Sie ihn in das Steuerelement ein. Benutzereingaben müssen beim paste und drop Ereignissen überprüft werden. Der in diesem Artikel beschriebene Code funktioniert in Angular 4 und höher, einschließlich FormHooks . Wenn Sie möchten, können Sie Unterstützung für Angular 2 hinzufügen, wenn Sie Renderer und nicht Renderer2 . Der Quellcode und das npm-Paket sind unter den folgenden Links verfügbar:


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


Und spielen Sie hier mit einem Beispiel:
https://stackblitz.com/edit/angular2-contenteditable-value-accessor

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


All Articles