¿Alguna vez te has preguntado cómo funciona la combinación de formas angulares y elementos HTML a través de los cuales el usuario ingresa datos?
Desde el principio utilizamos ControlValueAccessor
, una interfaz especial que consta de solo 4 métodos:
interface ControlValueAccessor { writeValue(value: any): void registerOnChange(fn: (value: any) => void): void registerOnTouched(fn: () => void): void setDisabledState(isDisabled: boolean)?: void }
Fuera de la caja, Angular tiene varios de estos accesos: para casillas de verificación y botones de opción, para entrada y selección. Sin embargo, si está desarrollando un chat en el que necesita dar la oportunidad de escribir en cursiva, poner el texto en negrita o, por ejemplo, insertar emoticones, lo más probable es que use el atributo contenteditable
para crear contenido formateado.
Angular no admite el uso de formularios junto con contenteditable
, por lo que debe escribirlo usted mismo.

ControlValueAccessor
La directiva que escribimos funcionará de manera similar a los accesores incorporados: responderá al atributo contenteditable
. Para que la plantilla y las formas reactivas lo reciban a través de la inyección de dependencia, es suficiente proporcionar un InjectionToken
incorporado:
@Directive({ selector: '[contenteditable][formControlName],' + '[contenteditable][formControl],' + '[contenteditable][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ContenteditableValueAccessor), multi: true, }, ], }) export class ContenteditableValueAccessor implements ControlValueAccessor {
La interfaz ControlValueAccessor
requiere 3 métodos y tiene 1 método opcional:
registerOnChange
: una función entrará en este método durante la inicialización. Se llama con un nuevo valor cuando el usuario ha ingresado algo en nuestro componente para que se ingresen nuevos datos en el control.registerOnTouched
: una función entrará en este método durante la inicialización. Debe llamarse cuando el usuario dejó nuestro componente para que se touched
el control. Esto se usa para la validación.writeValue
: el control writeValue
este método para pasar el valor a nuestro componente. Se usa si el valor cambia a través del código externo ( setValue
o cambiando la variable a la que está vinculado ngModel
), así como para establecer el valor inicial.
Vale la pena señalar que, a diferencia de las formas reactivas, ngModel
comporta de forma ngModel
, en particular, el valor inicial se inicializa con un retraso, y el método writeValue
"se contrae" dos veces, la primera vez con null
:
https://github.com/angular/angular/issues/14988
setDisabledState
(opcional) : el control llamará a este método cuando cambie el estado disabled
. Aunque el método es opcional, es mejor responder a esto en su componente.
Implementación de interfaz
Para trabajar con el elemento DOM, necesitamos Renderer2
y, de hecho, el elemento en sí mismo, así que agréguelos al constructor:
constructor( @Inject(ElementRef) private readonly elementRef: ElementRef, @Inject(Renderer2) private readonly renderer: Renderer2, ) {}
Guardamos los métodos que el control nos pasará en los campos privados de la clase:
private onTouched = () => {}; private onChange: (value: string) => void = () => {}; registerOnChange(onChange: (value: string) => void) { this.onChange = onChange; } registerOnTouched(onTouched: () => void) { this.onTouched = onTouched; }
disabled
estado disabled
para el componente contenteditable
es equivalente a deshabilitar el modo de edición - contenteditable="false"
. Establecer el valor desde el exterior es equivalente a reemplazar el DOM innerHTML
HTML del elemento, y actualizar el valor por parte del usuario y abandonar el componente se puede monitorear suscribiéndose a los eventos correspondientes:
@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, ); }
Eso, de hecho, es todo. Esto es suficiente para la implementación básica del trabajo de formas angulares y elementos contenteditable
.
Explorador de internet
Sin embargo, hay una pareja pero.
En primer lugar, el valor inicial vacío del formulario es null
, y después de writeValue
en IE11, veremos null
en la plantilla. Para implementar correctamente el trabajo, necesitamos normalizar el valor:
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; }
Aquí también manejaremos la siguiente situación. Imagine que el contenido de un elemento tiene etiquetas HTML. Si solo seleccionamos todo y lo eliminamos, no estará vacío en el interior: se insertará una etiqueta <br>
solitaria allí. Para no obstruir el control con un valor vacío, lo consideraremos como una cadena vacía.
En segundo lugar, Internet Explorer no admite eventos de input
para elementos contenteditable
. Tendremos que implementar el respaldo utilizando 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(); }
No implementaremos la verificación en un navegador específico. En cambio, deshabilitamos inmediatamente MutationObserver
en el primer evento de input
:
@HostListener('input') onInput() { this.observer.disconnect();
¡Ahora nuestro componente funciona en IE11 y estamos satisfechos con nosotros mismos!
Internet Explorer! ლ (ಠ 益 ಠ ლ)
Desafortunadamente, IE11 simplemente no se quedará atrás. Aparentemente, hay un error en el trabajo de MutationObserver
. Si hay etiquetas dentro del elemento contenteditable
, por ejemplo, some <b>text</b>
, al eliminar texto que implicará eliminar toda la etiqueta (la palabra text
en este ejemplo), se llamará a la devolución de llamada del observador antes de la real cambios en el DOM!

Desafortunadamente, no tenemos más remedio que admitir la derrota y usar setTimeout
:
private readonly observer = new MutationObserver(() => { setTimeout(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); });
Conclusión
Siempre que Angular sea compatible con las versiones 9, 10 y 11 de Internet Explorer, queda claro por qué no implementaron el trabajo con contenido contenteditable
en el hogar.
Además, debe recordar que el HTML puede contener código malicioso; por lo tanto, no tome audazmente contenido desconocido y péguelo en el control, y la entrada del usuario debe verificarse en drop
eventos de paste
y drop
. El código descrito en este artículo funciona en Angular 4 y superior, incluidos FormHooks
. Si lo desea, puede agregar soporte para Angular 2, si usa Renderer
, no Renderer2
. El código fuente y el paquete npm están disponibles en los enlaces:
https://github.com/TinkoffCreditSystems/angular-contenteditable-accessor
https://www.npmjs.com/package/@tinkoff/angular-contenteditable-accessor
Y juega con un ejemplo aquí:
https://stackblitz.com/edit/angular2-contenteditable-value-accessor