Vue.jsでWYSIWYGエディタコンポーネントを作ろう

慶應のアドベントカレンダーの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で吐き出された結果です。
上二つを編集すれば、全て同期されていますので、他のフィールドも同時に変更されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602951/12c84d50-1d7f-3c4a-765f-1854b1450e99.png)

##ソースコード
div#appに親のコンポーネントをmountして、その中にwysiwygコンポーネントを入れているというのが、基本的な構造です。SPA等であれば、そのページにinputタグのようにwysiwygをimportして使うという状況を想定しています。
とりあえず(ミニマルな)最終的なコードを掲載します。

```html
<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>


```
```js
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を使う」みたいなことを調べてばすぐ検索で出てくるので割愛します。

```js
props: ['value'],
model: {
  prop: 'value',
  event: 'change'
},
```

### templateで at input を使っている理由

```js
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を使っています。

```js
sync(ev) {
  this.$emit('change',ev.target.innerHTML)
},
```
実は

```js
this.value = hoge
```
とかしてしまうと "Avoid mutating a prop directly" と怒られてしまいます。
mutateしないで、イベントで変更しなければならないです。理由はエラーの中に書いているので、気になる人はエラーを起こしてみてはいかがでしょうか！！

```js
this.value.splice(適当)
```
とかもmutateするので、さけた方が良いのかもしれないですね。
僕だったら deep copy してからspliceして、変更後のやつを$emitに渡します。
___自作v-modelでは親にデータの変更を通知するときは、$emitを使いましょう！___

##最後
もし自分でリッチなメモアプリとかを作ってみたいとか、CMSを作りたい方がいれば、参考になればなと思っています。
また、個人的にはCMS開発はVue(JS)の良い勉強になったので、是非はお勧めしたいです！！