入力した内容によって「可変するようなデザイン」のフォーム要素を作る際に非常に便利な contenteditable
。
input
やtextarea
では表現しづらいデザインを簡単に実現できます。
しかし、vue
で、子供がcontenteditable
の場合、v-model
を使い親と双方向にsync
しようとすると、うまくいきません。何かしらのハックが必要です。
以下の記事が非常に参考になったのですが、「親/子両方で値を変更できる場合」にうまく行かず、もう少し拡張しました。
参考
https://qiita.com/zaru/items/baacf5eb490094aaa150
https://qiita.com/soumi/items/7d6d2e8c5616ccbdc665
動作イメージ
contenteditable では v-modelが動かない
親子でsyncするシンプルな input を contenteditable を置き換える、というシチュエーションを想像すると、以下のようなコードを思い浮かべると思います。
<template>
<div>
<p>親にあるinput(v-model)</p>
<input v-model="myValue">
<p>Child.vue内の可変するcontenteditable</p>
<Child :value.sync="myValue" />
</div>
</template>
<script>
import Child from '@/components/Child.vue'
export default {
components: { Child },
data() {
return {
myValue: '初期値'
}
}
}
</script>
以下のように、contenteditable
と v-model
を併用するコードでは動きません。
<template>
<div contenteditable="true" v-model="localValue"></div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: ''
},
},
computed: {
localValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('update:value', newValue)
}
}
}
}
</script>
参考記事で紹介されているように、キャレットが左に飛んでしまう問題の他に、
親子で同期してくれない問題を回避するために工夫したものが以下。
<template>
<div
contenteditable="true"
@input="update"
@focus="focus"
@blur="blur"
v-text="valueText"
></div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: ''
},
},
data() {
return {
focusIn: false,
valueText: ''
}
},
computed: {
localValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('update:value', newValue)
}
}
},
watch: {
localValue(newVal) {
// キャレットが動く問題を回避
if (!this.focusIn) {
this.valueText = newVal
}
}
},
created() {
// watchが走らない為、初回のみここで代入
this.valueText = this.value
},
methods: {
update(e) {
this.localValue = e.target.innerText
},
focus() {
this.focusIn = true
},
blur() {
this.focusIn = false
}
}
}
</script>
よりよい方法あるかもしれないが、一端メモ