Você já se perguntou como funciona a combinação de formulários angulares e elementos HTML através dos quais o usuário digita dados?
Desde o início, usamos o ControlValueAccessor
, uma interface especial que consiste em apenas 4 métodos:
interface ControlValueAccessor { writeValue(value: any): void registerOnChange(fn: (value: any) => void): void registerOnTouched(fn: () => void): void setDisabledState(isDisabled: boolean)?: void }
Fora da caixa, o Angular possui vários acessadores: para caixas de seleção e botões de opção, para entrada e seleção. No entanto, se você estiver desenvolvendo um bate-papo no qual precisará dar a oportunidade de escrever em itálico, deixar o texto em negrito ou, digamos, inserir emoticons, provavelmente usará o atributo contenteditable
para criar conteúdo formatado.
O Angular não suporta o uso de formulários junto com o contenteditable
, então você deve escrevê-lo.

ControlValueAccessor
A diretiva que escrevemos funcionará de maneira semelhante aos acessadores internos - ela responderá ao atributo contenteditable
. Para que o modelo e os formulários reativos o recebam por meio da injeção de dependência, basta fornecer um InjectionToken
embutido:
@Directive({ selector: '[contenteditable][formControlName],' + '[contenteditable][formControl],' + '[contenteditable][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ContenteditableValueAccessor), multi: true, }, ], }) export class ContenteditableValueAccessor implements ControlValueAccessor {
A interface ControlValueAccessor
requer 3 métodos e possui 1 método opcional:
registerOnChange
- uma função entrará nesse método durante a inicialização. É chamado com um novo valor quando o usuário digita algo em nosso componente para que novos dados sejam inseridos no controle.registerOnTouched
- uma função entrará nesse método durante a inicialização. Ele deve ser chamado quando o usuário deixou nosso componente para que o controle seja touched
. Isso é usado para validação.writeValue
- esse método será chamado pelo controle para passar o valor ao nosso componente. É usado se o valor for alterado pelo código externo ( setValue
ou alterando a variável à qual ngModel
está vinculado), bem como para definir o valor inicial.
Vale ressaltar que, diferentemente das formas reativas, o ngModel
se comporta de maneira ngModel
- em particular, o valor inicial nele é inicializado com um atraso e o método writeValue
"se writeValue
" duas vezes, a primeira vez com null
:
https://github.com/angular/angular/issues/14988
setDisabledState
(opcional) - esse método será chamado pelo controle quando o estado disabled
alterado. Embora o método seja opcional, é melhor responder a isso em seu componente.
Implementação de interface
Para trabalhar com o elemento DOM, precisamos do Renderer2
e, de fato, do próprio elemento; portanto, adicione-os ao construtor:
constructor( @Inject(ElementRef) private readonly elementRef: ElementRef, @Inject(Renderer2) private readonly renderer: Renderer2, ) {}
Salvamos os métodos que o controle nos passará nos campos particulares da classe:
private onTouched = () => {}; private onChange: (value: string) => void = () => {}; registerOnChange(onChange: (value: string) => void) { this.onChange = onChange; } registerOnTouched(onTouched: () => void) { this.onTouched = onTouched; }
estado disabled
para o componente contenteditable
é equivalente a desativar o modo de edição - contenteditable="false"
. Definir o valor de fora é equivalente a substituir o DOM innerHTML
do elemento, e atualizar o valor pelo usuário e deixar o componente pode ser monitorado, assinando os eventos correspondentes:
@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, ); }
Isso, de fato, é tudo. Isso é suficiente para a implementação básica do trabalho de formas angulares e elementos contenteditable
conteúdo.
Internet Explorer
No entanto, há um par, mas.
Primeiro, o valor inicial vazio do formulário é null
e, após writeValue
no IE11, veremos null
no modelo. Para implementar corretamente o trabalho, precisamos normalizar o 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; }
Aqui também trataremos da seguinte situação. Imagine que o conteúdo de um elemento tenha tags HTML. Se apenas selecionarmos tudo e o excluirmos, ele não ficará vazio por dentro - uma tag solitária será inserida lá. Para não obstruir o controle com um valor vazio, consideraremos como uma string vazia.
Em segundo lugar, o Internet Explorer não suporta eventos de input
para elementos contenteditable
por contenteditable
. Teremos que implementar fallback usando 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(); }
Não implementaremos a verificação em um navegador específico. Em vez disso, desabilitamos MutationObserver
imediatamente no primeiro evento de input
:
@HostListener('input') onInput() { this.observer.disconnect();
Agora nosso componente funciona no IE11 e estamos satisfeitos conosco!
Internet Explorer! ლ (ಠ 益 ಠ ლ)
Infelizmente, o IE11 não será deixado para trás. Aparentemente, há um erro no trabalho do MutationObserver
. Se houver tags dentro do elemento contenteditable
, por exemplo, some <b>text</b>
, ao excluir texto, o que implicará a exclusão de toda a tag (a palavra text
neste exemplo), o retorno de chamada do observador será chamado antes do real mudanças no DOM!

Infelizmente, não temos escolha a não ser admitir a derrota e usar setTimeout
:
private readonly observer = new MutationObserver(() => { setTimeout(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); });
Conclusão
Desde que o Angular suporte o Internet Explorer versões 9, 10 e 11, fica claro por que eles não implementaram o trabalho com contenteditable
em casa.
Além disso, você deve se lembrar que o HTML pode conter código malicioso - portanto, não use negativamente o conteúdo desconhecido e cole-o no controle, e a entrada do usuário deve ser verificada nos eventos paste
e drop
. O código descrito neste artigo funciona no Angular 4 e superior, incluindo o FormHooks
. Se desejar, você pode adicionar suporte ao Angular 2, se usar o Renderer
, não o Renderer2
. O código fonte e o pacote npm estão disponíveis nos links:
https://github.com/TinkoffCreditSystems/angular-contenteditable-accessor
https://www.npmjs.com/package/@tinkoff/angular-contenteditable-accessor
E brinque com um exemplo aqui:
https://stackblitz.com/edit/angular2-contenteditable-value-accessor