Angular6自定義表單控件方式集成Editormd

曾經找到過“Editor.md”,看之心喜,一直想在Angular中集成下這款markdownpad編輯器玩,在網上也只找到一篇通過指令集成的,雖然可以實現,但還是希望能做成組件形式的,之後看到一篇自定義組件的文章,瞭解到ControlValueAccessor才真正完成這個心願,現在記錄分享與諸公。

ControlValueAccessor

這是自定義表單組件的核心,只有繼承這個接口,纔有被 Angular的formControl識別的資格。

ControlValueAccessor要處理的就是實現 Model -> View,View -> Model 之間的數據綁定,其具體的作用是:

  • 把 form 模型中值映射到視圖中
  • 當視圖發生變化時,通知 form directives 或 form controls

該接口具體如下,已去掉其中的英文註釋:

export interface ControlValueAccessor {

    writeValue(obj: any): void;

    registerOnChange(fn: any): void;

    registerOnTouched(fn: any): void;

    setDisabledState?(isDisabled: boolean): void;
}
  • writeValue:在初始化的時候將formControl的值傳遞給原生表單控件(即,將模型中的新值寫入視圖或 DOM 屬性中);
  • registerOnChange:用來獲取原生表單控件的值更新時通知Angular表單控件更新的函數(即,設置當控件接收到 change 事件後,調用的函數)
  • registerOnTouched:用來獲取通知用戶正在交互的函數(即,設置當控件接收到 touched 事件後,調用的函數)。
  • setDisabledState?(isDisabled: boolean):設置DISABLED狀態時做的執行的方法。即,當控件狀態變成 DISABLED 或從 DISABLED 狀態變化成 ENABLE 狀態時,會調用該函數。該函數會根據參數值,啓用或禁用指定的 DOM 元素。

明確來說,那些原生表單控件都有其對應的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,在正式開始前還需要最後的準備工作:

  1. 使用npm或者yarn安裝jquery
npm install jquery
或者
yarn add jquery
  1. 下載Editor.md
  2. 將需要的css、fonts、images、lib、plugins三個文件夾和editormd.min.js文件放入assets中(其他位置也可,記得配置第3步中對應的angular.json),這裏添加的是精簡資源,也可以把解壓出來的全部放進去,效果如圖:
  1. 配置angular.json
"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"
            ]

創建EditorMdComponent

該組件肯定要繼承ControlValueAccessor,首先是實現其上面的方法。

writeValue

  writeValue(value: string): void {
    this.value = value;
    if (this.mdeditor) {
        this.mdeditor.setMarkdown(this.value);
    }
  }

registerOnChange

  onChange: Function = () => { };
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

registerOnTouched

本示例中實際未用的該方法,主要是registerOnChange。

  onTouched: Function = () => { };
    registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

setDisabledState

這個也未使用,即便設置也會報mdeditor未知的錯誤,禁用功能需要使用其他方式解決。

  setDisabledState?(isDisabled: boolean): void {
    if (isDisabled) {
      this.mdeditor.setDisabled();
    } else {
        this.mdeditor.setEnabled();
    }
  }

AfterViewInit

我們需要執行初始化編輯器的操作,故實現了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

爲了安全週期,實現了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
};

最終代碼

EditorMdComponent

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

EditorConfig

此爲默認編輯器配置。

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>

參考資料

Angular 4.x 自定義表單控件

【薦】深入Angular自定義表單控件

Angular集成Editor.md的Markdown編輯器,支持NgModel雙向綁定

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章