曾经找到过“Editor.md”,看之心喜,一直想在Angular中集成下这款markdownpad编辑器玩,在网上也只找到一篇通过指令集成的,虽然可以实现,但还是希望能做成组件形式的,之后看到一篇自定义组件的文章,了解到ControlValueAccessor才真正完成这个心愿,现在记录分享与诸公。
这是自定义表单组件的核心,只有继承这个接口,才有被 Angular的formControl识别的资格。
ControlValueAccessor要处理的就是实现 Model -> View,View -> Model 之间的数据绑定,其具体的作用是:
该接口具体如下,已去掉其中的英文注释:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
明确来说,那些原生表单控件都有其对应的ControlValueAccessor,比如: - DefaultValueAccessor - 用于 text 和 textarea 类型的输入控件 - SelectControlValueAccessor - 用于 select 选择控件 - CheckboxControlValueAccessor - 用于 checkbox 复选控件
至于原生表单控件和Angular表单控件能够保持一致的原理,可以看下formControl指令的实现:
// https://github.com/angular/angular/blob/master/packages/forms/src/directives/reactive_directives/form_control_directive.ts
export class FormControlDirective extends NgControl implements OnChanges {
...
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
if (this.control.disabled && this.valueAccessor !.setDisabledState) {
this.valueAccessor !.setDisabledState !(true);
}
this.form.updateValueAndValidity({emitEvent: false});
}
if (isPropertyUpdated(changes, this.viewModel)) {
_ngModelWarning(
'formControl', FormControlDirective, this, this._ngModelWarningConfig);
this.form.setValue(this.model);
this.viewModel = this.model;
}
}
...
}
这里仅列出了部分实现,formControl指令调用了setUpControl函数来实现formControl和ControlValueAccessor之间的交互。
// https://github.com/angular/angular/blob/master/packages/forms/src/directives/shared.ts
...
dir.valueAccessor !.writeValue(control.value);
...
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
...
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor !.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
...
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor !.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
...
里面确实能看到一些似曾相识的方法,但个人能力有限,无法完全看懂,也就只能到这里了,喜欢深入探究的可以自行探索。
经过上面大致了解ControlValueAccessor,在正式开始前还需要最后的准备工作:
npm install jquery
或者
yarn add jquery
"styles": [
"src/assets/editorMd/css/editormd.min.css",
"node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"src/assets/editorMd/editormd.min.js"
]
该组件肯定要继承ControlValueAccessor,首先是实现其上面的方法。
writeValue(value: string): void {
this.value = value;
if (this.mdeditor) {
this.mdeditor.setMarkdown(this.value);
}
}
onChange: Function = () => { };
registerOnChange(fn: any): void {
this.onChange = fn;
}
本示例中实际未用的该方法,主要是registerOnChange。
onTouched: Function = () => { };
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
这个也未使用,即便设置也会报mdeditor未知的错误,禁用功能需要使用其他方式解决。
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.mdeditor.setDisabled();
} else {
this.mdeditor.setEnabled();
}
}
我们需要执行初始化编辑器的操作,故实现了AfterViewInit。
@ViewChild('host') host; // hmtl中添加 #host标识,用于选择组件模板内的节点
ngAfterViewInit(): void {
this.init();
}
init() {
if (typeof editormd === 'undefined') {
console.error('UEditor is missing');
return;
}
// 检测配置,若无自定义,则用默认配置。
this.editormdConfig = this.editormdConfig != null ? this.editormdConfig : new EditorConfig();
// 监听编辑器加载完成事件处理,由于该编辑器的配置特性,只能提前写好传入。这里是用来处理存在默认值时。
this.editormdConfig.onload = () => {
if (this.value) {
this.mdeditor.setMarkdown(this.value);
}
};
// 变化监听处理
this.editormdConfig.onchange = () => {
this.updateValue(this.mdeditor.getMarkdown());
};
// 编辑器必须使用<div id></div>的形式,所以只好添加默认id,后期可考虑传入自定义id
this.mdeditor = editormd(this.host.nativeElement.id, this.editormdConfig); // 创建编辑器
}
updateValue(value: string) {
this.ngZone.run(() => {
this.value = value;
this.onChange(this.value); // 关键代码
this.onTouched();
this.onValueChange.emit(this.value);
this.getHtmlValue.emit({ originalEvent: event, value: this.getHtmlContent() });
});
}
为了安全周期,实现了OnDestroy
ngOnDestroy(): void {
this.destroy();
}
destroy() {
if (this.mdeditor) {
this.mdeditor.removeListener('ready');
this.mdeditor.removeListener('contentChange');
this.mdeditor.editor.remove();
this.mdeditor.destroy();
this.mdeditor = null;
}
}
其中useExisting用来设置验证函数,可自定义:
const UEDITOR_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EditorMdComponent),
multi: true
};
import { Component, ViewChild, Input, AfterViewInit, ElementRef, NgZone, Output, EventEmitter, OnDestroy, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { EditorConfig } from './editor-config';
declare var editormd: any;
const UEDITOR_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EditorMdComponent),
multi: true
};
@Component({
selector: 'qy-editor-md',
templateUrl:`
<div id="md" #host > </div>
`,
styleUrls: ['./editor-md.component.scss'],
providers: [UEDITOR_VALUE_ACCESSOR]
})
export class EditorMdComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
@Input() editormdConfig: EditorConfig; // 配置选项
// tslint:disable-next-line:no-output-on-prefix
@Output() onReady = new EventEmitter();
// tslint:disable-next-line:no-output-on-prefix
@Output() onValueChange = new EventEmitter();
// tslint:disable-next-line:no-output-on-prefix
@Output() onFocus = new EventEmitter();
@Output() getHtmlValue = new EventEmitter();
@ViewChild('host') host;
private mdeditor: any;
private value: string;
onChange: Function = () => { };
onTouched: Function = () => { };
constructor(
private el: ElementRef,
private ngZone: NgZone
) {
}
ngAfterViewInit(): void {
this.init();
}
ngOnDestroy(): void {
this.destroy();
}
writeValue(value: string): void {
this.value = value;
console.log('value', value);
if (this.mdeditor) {
this.mdeditor.setMarkdown(this.value);
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.mdeditor.setDisabled();
} else {
this.mdeditor.setEnabled();
}
}
init() {
if (typeof editormd === 'undefined') {
console.error('UEditor is missing');
return;
}
this.editormdConfig = this.editormdConfig != null ? this.editormdConfig : new EditorConfig();
this.editormdConfig.onload = () => {
if (this.value) {
this.mdeditor.setMarkdown(this.value);
}
};
this.editormdConfig.onchange = () => {
this.updateValue(this.mdeditor.getMarkdown());
};
this.mdeditor = editormd(this.host.nativeElement.id, this.editormdConfig); // 创建编辑器
}
updateValue(value: string) {
this.ngZone.run(() => {
this.value = value;
this.onChange(this.value);
this.onTouched();
this.onValueChange.emit(this.value);
this.getHtmlValue.emit({ originalEvent: event, value: this.getHtmlContent() });
});
}
destroy() {
if (this.mdeditor) {
this.mdeditor.removeListener('ready');
this.mdeditor.removeListener('contentChange');
this.mdeditor.editor.remove();
this.mdeditor.destroy();
this.mdeditor = null;
}
}
getMarkContent(): string {
return this.mdeditor.getMarkdown();
}
getHtmlContent(): string {
console.log('this.mdeditor.getHTML() 1', this.mdeditor.getHTML());
return this.mdeditor.getHTML();
}
}
此为默认编辑器配置。
export class EditorConfig {
public width = '100%';
public height = '400';
public path = 'assets/editorMd/lib/';
public codeFold: true;
public searchReplace = true;
public toolbar = true;
public emoji = true;
public taskList = true;
public tex = true;
public readOnly = false;
public tocm = true;
public watch = true;
public previewCodeHighlight = true;
public saveHTMLToTextarea = true;
public markdown = '';
public flowChart = true;
public syncScrolling = true;
public sequenceDiagram = true;
public imageUpload = true;
public imageFormats = ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'];
public imageUploadURL = '';
constructor() {
}
public onload() {
}
public onchange() {
}
}
最后记得按照正常组件进行引入和声明才可使用哦。
之后就可以在表单组件中可以直接引入了:
<qy-editor-md formControlName="comment" (getHtmlValue)="getHtmlValue($event)" ></qy-editor-md>