Angular结合quill实现富文本编辑器

1. 前言

由于需要一个富文本编辑器来编辑一些网页内容, 手动编辑后存储到数据库比较麻烦, 所以着手实现一个自己的富文本编辑器, 来编辑和存储一些 html 文件.
这里使用 Angular 框架, 加 Quill 库实现.

ngx-quill: https://github.com/KillerCodeMonkey/ngx-quill
quill 官网: https://quilljs.com/

2. 创建 Angular 工程

2.1. 创建工程

首先创建一个 angular 工程. 工程的名字就叫 angular-editor.

1
2
3

ng new angular-editor

2.2. 添加依赖

这里需要添加 ngx-quill 依赖包, 以下是 ngx-quill 与 Angular 之间的兼容关系.

Angularngx-quillsupported
v15>= 20.0.0until May, 2024
v14>= 17.0.0until Dec 02, 2023
v13>= 15.0.0, < 17.0.0until May 04, 2023

由于我目前使用的 angular 版本 13.3.11, 我选择了一个稳定版本ngx-quill@16.2.1
查看ngx-quill@16.2.1的配置文件package.json, 其对应的 quill 版本为quill@1.3.7, 所以这里 quill 使用quill@1.3.7. 为了让 typescript 能识别类型信息, 这里还需要导入一个开发依赖包@types/quill@1.3.10, 版本也可以从 ngx-quill 的 package.json 中找到.

1
2
3
4
5

npm install ngx-quill@16.2.1 --save
npm install quill@1.3.7 --save
npm install @types/quill@1.3.10 --save-dev

当前最新版本为 ngx-quill@20.0.1 quill@1.3.7

3. 创建编辑器

3.1. 引入 Quill 模块

添加依赖包之后还不能直接使用 Quill, 还需要再使用 Quill 的 Module 声明文件引入它.

以下以根模块为例讲解如何引入模块,引入 ngx-quill 的 QuillModule

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { QuillModule } from "ngx-quill"; // 引入富文本编辑器模块
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
QuillModule.forRoot(), // 富文本编辑器模块
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

3.2. 引入 quill css 样式

由于我使用的 scss, 所以可以很方便的引入 quill css. 可以在预编译的时候将 quill 的样式编译进来.

找到 styles.scss, 添加如下代码引入 quill 样式

quill 提供两种主题, 一种是 bubble, 另一种是 snow, 默认是 snow, 可以任选一种导入, 也可以同时导入两种主题, 方便动态切换样式.

styles.scss

1
2
@import url("https://cdn.quilljs.com/1.3.7/quill.snow.css");
@import url("https://cdn.quilljs.com/1.3.7/quill.bubble.css");

3.3. 将 quill 富文本编辑器添加到页面

做好以上准备工作, 我们就可以将 quill 富文本编辑器. 以 app.component.html 为例, 只要在页面添加这样一行, 然后启动 angular 应用, 就可以看到编辑器了.

1
<quill-editor></quill-editor>

启动应用看效果

1
ng serve --open

4. 加载与获取富文本内容

当我们使用 quill 编辑文档的时候, 往往不是从空白文档开始, 大多数情况下我们是在已有文档的基础上进行修改.

当我们拿到一个文档时, 如何将其内容加载到 quill 编辑器中呢? 网上很多的教程或博客讲解得不够深入.

首先要将 quill-editor 与一个 control 控件连接起来, 如下:

1
2
3
<form [formGroup]="form">
<quill-editor format="html" formControlName="html"></quill-editor>
</form>

当然连接之前, 我们需要创建该控件.

1
2
3
4
5
form: FormGroup = this.fb.group({
html: new FormControl(
'<div>test</div><ul><li>1</li><li class="ql-indent-1">1-1</li><li>2</li><ol><li>numbered</li><li class="ql-indent-1">numbered-1</li></ol></ul><div><br></div>'
),
});

这样我们通过控制该控件, 可以在构建 FormControl 时传入, 也可以在创建完 FormControl 后通过 setValue 方法加载 html 内容, 就能加载 html 内容到 quill 编辑器中. 而在 quill 中编辑文档内容时, control 控件中的内容会自动更新.

当我们准备存盘时, 获取到该控件, 通过 value 熟悉即可获取到修改后的 html 内容 form.get(‘html’).value, 而不必去操作 quill-editor 组件.

更多用法可以参考ngx-quill 示例

5. 如何处理插入图片

当插入图片时, quill 默认会将图片转换成 base64 编码嵌入到文本中, 因为我需要将富文本存储到数据库中, 这种默认方式会导致数据库字段内容庞大.
影响查询性能. 所以我想将这种默认行为改为, 将图片保存到图片服务器, 在富文本中仅仅插入图片链接.

首先需要捕获 onEditorCreated 事件, 捕获该事件后我们才有机会替换 quill 编辑器的默认行为. 获取该事件的方法十分简单.
只需要给 qull-editor 绑定一个定制的方法editorCreated($event), 通过$event 即可获取到创建好的 editor 本身.

1
2
3
4
5
<quill-editor
format="html"
formControlName="body"
(onEditorCreated)="editorCreated($event)"
></quill-editor>

捕获到 onEditorCreated 事件以及获取到 editor 后我们就可以客制化插入图片的行为了.

1
2
3
4
5
6
7
8
9
/**
* ngx-quill上传图片需要的方法
*/
editorCreated(quill:any) {
const toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', this.imageHandler); // 将image handler替换为自己的imageHander
this.editor = quill;
}

将 image handler 替换为自己的 imageHander. 例如, 一下是我实现的一个 imageHandler.
实现方式比较容易理解, 即将图片上传到文件服务器, 然后获取到图片的 url, 将 url 嵌入到图片插入位置.

这里 imageHandler 做的事情很简单, 只是出发一个 open dialog 事件.
为什么这样设计? 因为在 imageHandler 内部调用 this.dialog.open 创建的 DialogOverviewExampleDialog 脱离了 NgZone, 后续无论是渲染, 还是关闭对话框都会出现很奇怪的行为.
所以在 imageHandler 内部只是触发一个事件.外部的 component 接收到这个事件再打开对话框.

1
2
3
4
5
6
7
8
9
10
11
12
13
 /**
* Note: why not dirrectly call open dialog that's because
* the mothod need to bind this which will cause
* the problem that the component created by this.dialog.open
* will be out of box (ngzone)
* please refer to the page for the details
* https://github.com/angular/components/issues/9676
*/
imageHandler(){
const event = new Event("open dialog");
window.dispatchEvent(event);
}

外部的组件也就是 AppComponent 接收到事件再弹出对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@HostListener("window:open dialog")
openDialog() {

let dialogRef = this.dialog.open(DialogOverviewExampleDialog, {width:'400px'});

dialogRef.afterClosed().subscribe(result => {
console.log(result);
if(result) {
const range = this.editor.getSelection(true);
const index = range.index + range.length;
this.editor.insertEmbed(index, 'image', result, 'user');
this.editor.setSelection(1+index)
}
});
}

这里要自己设计对话框组件 DialogOverviewExampleDialog, 关闭时传出图片的 URL;

可以参照如下代码

example-dialog.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, NgZone, OnInit } from "@angular/core";
import { MatDialogRef } from "@angular/material/dialog";

@Component({
selector: "dialog-overview-example-dialog",
templateUrl: "example-dialog.component.html",
styleUrls: ["./example-dialog.component.scss"],
})
export class DialogOverviewExampleDialog implements OnInit {
value = "";
constructor(
public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
public ngZone: NgZone
) {}

ngOnInit(): void {}

close(): void {
console.log("close clicked");
this.dialogRef.close();
}
}

example-dialog.component.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div cdkDrag cdkDragRootElement=".cdk-overlay-pane" class="w-100">
<h1 mat-dialog-title>Insert image</h1>
<div mat-dialog-content>
<mat-form-field class="w-100">
<mat-label>url</mat-label>
<input
type="text"
placeholder="input image url"
matInput
[(ngModel)]="value"
/>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-button (click)="close()">Cancel</button>
<button mat-button [mat-dialog-close]="value" cdkFocusInitial>Ok</button>
</div>
</div>

6. 实现后的效果

实现后的效果如下:

angular quill editor

7. Angular 系列文章

最新更新以及更多 Angular 相关文章请访问 Angular 合集 | 鹏叔的技术博客

8. 参考文档

Angular:ngx-quill 富文本编辑器的使用

如何在 Angular 11/12 版本中整合 ngx-quill 教程

element ui 富文本编辑器的使用

Dialog

Handler for image upload

angular-demo