慶應のアドベントカレンダーの24日目として書かしてもらいました!誘ってくださったSさん、ありがとうございます。
https://adventar.org/calendars/6279
#Vue.js で WYSIWYG エディタコンポーネントを作ろう
オリジナルのCMSを作りたい時に、inputやtextareaのような使い心地で入力フィールドを作ることができないかを考えていました。コンポーネントを作る上での目標は、
v-modelを適用するだけでHTMLの文字列データと同期できること
です。あまり時間がないので、軽めの内容になってしまいましたが、ご容赦ください。
今回は JSFiddle(https://jsfiddle.net/) にコードを書いていきますので、適宜単一ファイルのコンポーネントに置き換えて使ってください。
##出来上がりイメージ
一番上のフィールドがcontenteditableがtrueになっている要素です。ここで文字を打ち替えたりできます。選択範囲(Range)を指定してspanとかで包む関数を作れば、MSの Sway みたいにできます。
二つ目が生のHTMLをそのまま編集できるフィールドで、最後がv-htmlで吐き出された結果です。
上二つを編集すれば、全て同期されていますので、他のフィールドも同時に変更されます。
##ソースコード
div#appに親のコンポーネントをmountして、その中にwysiwygコンポーネントを入れているというのが、基本的な構造です。SPA等であれば、そのページにinputタグのようにwysiwygをimportして使うという状況を想定しています。
とりあえず(ミニマルな)最終的なコードを掲載します。
<div id="app">
<div class="container">
<h2>WYSIWYG</h2>
<wysiwyg v-model="html" ref="wysiwygcomp" />
</div>
<div class="container">
<h2>raw HTML</h2>
<textarea type="text" v-model="html" class="raw-text-editor"></textarea>
</div>
<div class="container">
<h2>v-html</h2>
<div v-html="html"></div>
</div>
</div>
const wysiwyg = Vue.extend({
template: `
<div class="wysiwyg-editor"
contenteditable="true"
ref="wysiwyg"
@input="sync"
></div>`,
props: ['value'],
model: {
prop: 'value',
event: 'change',
},
watch: {
value(v) {
this.sync2innerhtml()
}
},
mounted() {
this.sync2innerhtml()
},
methods: {
sync(ev) {
this.$emit('change',ev.target.innerHTML)
},
sync2innerhtml() {
this.$refs.wysiwyg.innerHTML = this.value
}
}
})
new Vue({
el: "#app",
components: { wysiwyg },
data: {
html: `Hello, <span style="color: blue;">World</span>`
},
methods: {
sync(e) {
// sync2wysiwygから読んでも一つ前が同期されてしまう
this.$refs.wysiwygcomp.sync(e.target.value)
}
}
})
##説明
小分けにしてなるべく頑張って説明します!!(あんまり説明することもない気がしますが!!)
v-modelの適用
↓この辺は、「自作コンポーネントにv-modelを使う」みたいなことを調べてばすぐ検索で出てくるので割愛します。
props: ['value'],
model: {
prop: 'value',
event: 'change'
},
templateで at input を使っている理由
template: `
<div class="wysiwyg-editor"
contenteditable="true"
ref="wysiwyg"
@input="sync"
></div>`,
関数でinnerHTMLを変えることもあるわけだし、@change
ができたら楽だなとは思っていたんですけど、innerHTMLを編集してもchangeが発火されないんですよね。Mutation Observerとか使って検知できそうですが、なんかうまくいかなかったので、inputでいいやとなりました。ごめんなさい。@input
はちゃんと動いてくれます。
watchしている理由
親のデータ(html)を書き換えていても、innerHTMLは勝手に変わってはくれません。変更を検知して、innerHTMLを変更して初めてWYSIWYGエディタのような振る舞いをしてくれるのです。
$emitでデータを変更しよう
以下のように親のデータと同期するために$emitを使っています。
sync(ev) {
this.$emit('change',ev.target.innerHTML)
},
実は
this.value = hoge
とかしてしまうと "Avoid mutating a prop directly" と怒られてしまいます。
mutateしないで、イベントで変更しなければならないです。理由はエラーの中に書いているので、気になる人はエラーを起こしてみてはいかがでしょうか!!
this.value.splice(適当)
とかもmutateするので、さけた方が良いのかもしれないですね。
僕だったら deep copy してからspliceして、変更後のやつを$emitに渡します。
自作v-modelでは親にデータの変更を通知するときは、$emitを使いましょう!
##最後
もし自分でリッチなメモアプリとかを作ってみたいとか、CMSを作りたい方がいれば、参考になればなと思っています。
また、個人的にはCMS開発はVue(JS)の良い勉強になったので、是非はお勧めしたいです!!