您是否曾经想过用户输入数据所使用的Angular表单和HTML元素的组合是如何工作的?
从一开始,我们就使用ControlValueAccessor
,这是一个仅包含4种方法的特殊接口:
interface ControlValueAccessor { writeValue(value: any): void registerOnChange(fn: (value: any) => void): void registerOnTouched(fn: () => void): void setDisabledState(isDisabled: boolean)?: void }
Angular开箱即用,有几个这样的访问器:用于复选框和单选按钮,用于输入和选择。 但是,如果您正在进行聊天,需要在其中提供机会以斜体书写,将文本加粗或插入表情符号,则很有可能会使用contenteditable
属性来创建格式化的内容。
Angular不支持将表单与contenteditable
一起使用,因此您必须自己编写它。

ControlValueAccessor
我们编写的指令将与内置访问器类似地工作-它将响应contenteditable
属性。 为了使模板和反应形式通过依赖注入来接收它,提供一个内置的InjectionToken
就足够了:
@Directive({ selector: '[contenteditable][formControlName],' + '[contenteditable][formControl],' + '[contenteditable][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ContenteditableValueAccessor), multi: true, }, ], }) export class ContenteditableValueAccessor implements ControlValueAccessor {
ControlValueAccessor
接口需要3种方法,并具有1种可选方法:
registerOnChange
初始化期间将有一个函数进入此方法。 当用户在我们的组件中输入了一些内容,以便将新数据输入到控件中时,将使用新值调用它。registerOnTouched
在初始化期间,函数会进入此方法。 用户离开我们的组件时必须调用它,以使控件被touched
。 这用于验证。writeValue
控件将调用此方法以将值传递给我们的组件。 如果值通过外部代码更改( setValue
或更改ngModel
绑定的变量)以及设置初始值,则使用该值。
值得注意的是,与反应形式不同, ngModel
行为ngModel
-特别是,其中的初始值会延迟初始化,并且writeValue
方法会“抽动”两次,第一次是使用null
:
https://github.com/angular/angular/issues/14988
setDisabledState
(可选) - disabled
状态更改时,控件将调用此方法。 尽管该方法是可选的,但最好在组件中对此进行响应。
接口实现
要使用DOM元素,我们需要Renderer2
以及实际上的元素本身,因此将它们添加到构造函数中:
constructor( @Inject(ElementRef) private readonly elementRef: ElementRef, @Inject(Renderer2) private readonly renderer: Renderer2, ) {}
我们将控制将传递给我们的方法保存在类的私有字段中:
private onTouched = () => {}; private onChange: (value: string) => void = () => {}; registerOnChange(onChange: (value: string) => void) { this.onChange = onChange; } registerOnTouched(onTouched: () => void) { this.onTouched = onTouched; }
contenteditable
组件的disabled
状态等效于禁用编辑模式contenteditable="false"
。 将值设置为外部等效于替换元素的innerHTML
DOM,并且可以通过订阅相应事件来监视用户更新值并离开组件:
@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, ); }
实际上,仅此而已。 这对于Angular表单和可contenteditable
元素的基本实现就足够了。
互联网浏览器
但是,有一对。
首先,表格的空初始值为null
,在IE11中writeValue
之后,我们将在模板中看到null
。 为了正确执行工作,我们需要将值标准化:
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; }
在这里,我们还将处理以下情况。 想象一下,元素的内容具有HTML标记。 如果我们只是选择所有内容并将其删除,则里面的内容将不会为空-将会在其中插入一个<br>
标记。 为了不使用空值阻塞控件,我们将其视为空字符串。
其次,Internet Explorer不支持可contenteditable
元素的input
事件。 我们将必须使用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(); }
我们不会在特定的浏览器上实施验证。 相反,我们在第一个input
事件上立即禁用MutationObserver
:
@HostListener('input') onInput() { this.observer.disconnect();
现在我们的组件可以在IE11中运行,我们对自己感到满意!
Internet Explorer! ლ(ಠ益ಠლ)
不幸的是,IE11不会被遗忘。 显然, MutationObserver
的工作中存在一个错误。 如果contenteditable
元素内部有标签,例如some <b>text</b>
,则在删除需要删除整个标签的text
在本示例中为单词text
)时,将在实数之前调用观察者的回调。 DOM中的变化!

不幸的是,我们别无选择,只能认输并使用setTimeout
:
private readonly observer = new MutationObserver(() => { setTimeout(() => { this.onChange( ContenteditableValueAccessor.processValue( this.elementRef.nativeElement.innerHTML, ), ); }); });
结论
如果Angular应该支持Internet Explorer版本9、10和11,那么很清楚为什么他们不在家中使用contenteditable
实现工作。
另外,您需要记住HTML可能包含恶意代码-因此,请勿大胆获取未知内容并将其粘贴到控件中,并且必须在paste
事件中检查用户输入。 本文介绍的代码可在Angular 4及更高版本中使用,包括FormHooks
。 如果愿意,可以使用Renderer
而不是Renderer2
添加对Angular 2的支持。 链接中提供了源代码和npm软件包:
https://github.com/TinkoffCreditSystems/angular-contenteditable-accessor
https://www.npmjs.com/package/@tinkoff/angular-contenteditable-accessor
在这里玩一个例子:
https://stackblitz.com/edit/angular2-contenteditable-value-accessor