3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-24

慶應のアドベントカレンダーの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

##ソースコード
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)の良い勉強になったので、是非はお勧めしたいです!!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?