JavaScript
vue.js

Vue.js で contenteditable を v-model 風に使う

Vue.js でとても便利な v-model ですが contenteditable な要素では使えません。 v-model は内部的には v-bind:valuev-on:input をまとめてやってくれているからです。つまり input 要素じゃない contenteditable では効かないという感じです。

そこで自前でイベントを使って雑に書くとこんな風に書いてしまいますが、これには問題があります。入力するたびにキャレットが文字列の先頭に飛んでしまいます。これは v-text が更新されるため自身も render が走ってしまうのが原因です。かといって v-once を付けてしまうと、今度は自身を変更するすべがなくなってしまいます。

<template>

<div>
<div contenteditable="true" v-text="content" @input="sync"></div>
<div>{{ content }}</div>
</div>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
name: 'HelloWorld',
data () {
return {
content: 'sample text'
}
},
methods: {
sync (e) {
this.content = e.target.innerText
}
}
})
</script>

回避方法は至って単純で、もう一つ contenteditable 用の変数を用意すること。

<template>

<div class="hello">
<div contenteditable="true" v-text="innerContent" @input="sync"></div>
<div v-html="content"></div>
</div>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
name: 'HelloWorld',
data () {
return {
innerContent: '',
content: 'sample text'
}
},
methods: {
sync (e) {
this.content = e.target.innerHTML
}
},
mounted() {
this.innerContent = this.content
}
})
</script>

ついでにコンポーネントにしておきます。

<template>

<div contenteditable="true" v-text="innerContent" @input="sync"></div>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
name: 'Editor',
props: ['content'],
data () {
return {
innerContent: ''
}
},
methods: {
sync (e) {
this.$emit('update', e.target.innerHTML)
}
},
mounted() {
this.innerContent = this.content
}
})
</script>

<template>

<div class="hello">
<editor :content="content" @update="sync"></editor>
<div v-html="content"></div>
</div>
</template>

<script>
import Vue from 'vue'
import Editor from './Editor'

export default Vue.extend({
name: 'HelloWorld',
components: {
Editor
},
data () {
return {
content: 'sample text'
}
},
methods: {
sync (content) {
this.content = content
}
}
})
</script>

雑なサンプルですが、こんな感じで contenteditablev-model っぽいデータの同期ができます。