Vue 2 contentEditable with v-model
I think I may have come up with an even easier solution. See snippet below:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<main id="app">
<div class="container-fluid">
<div class="row">
<div class="col-8 bg-light visual">
<span class="text-dark m-0" v-html="content"></span>
</div>
<div class="col-4 bg-dark form">
<button v-on:click="bold_text">Bold</button>
<span class="bg-light p-2" contenteditable @input="handleInput">Change me!</span>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
new Vue({
el: '#app',
data: {
content: 'Change me!',
},
methods: {
handleInput: function(e){
this.content = e.target.innerHTML
},
bold_text: function(){
document.execCommand('bold')
}
}
})
</script>
</body>
</html>
Explanation:
You can edit the span as I have added the tag contenteditable
. Notice that on input
, I will call the handleInput function, which sets the innerHtml of the content to whatever you have inserted into the editable span. Then, to add the bold functionality, you simply select what you want to be bold and click on the bold button.
Added bonus! It also works with cmd+b ;)
Hopefully this helps someone!
Happy coding
Note that I brought in bootstrap css for styling and vue via CDN so that it will function in the snippet.
I tried an example, and eslint-plugin-vue reported that v-model
isn't supported on p
elements. See the valid-v-model rule.
As of this writing, it doesn't look like what you want is supported in Vue directly. I'll present two generic solutions:
Use input events directly on the editable element
<template>
<p
contenteditable
@input="onInput"
>
{{ content }}
</p>
</template>
<script>
export default {
data() {
return { content: 'hello world' };
},
methods: {
onInput(e) {
console.log(e.target.innerText);
},
},
};
</script>
Create a reusable editable component
Editable.vue
<template>
<p
ref="editable"
contenteditable
v-on="listeners"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs.editable.innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
},
},
};
</script>
index.vue
<template>
<Editable v-model="content" />
</template>
<script>
import Editable from '~/components/Editable';
export default {
components: { Editable },
data() {
return { content: 'hello world' };
},
};
</script>
Custom solution for your specific problem
After a lot of iterations, I found that for your use case it was easier to get a working solution by not using a separate component. It seems that contenteditable
elements are extremely tricky - especially when rendered in a list. I found I had to manually update the innerText
of each p
after a removal in order for it to work correctly. I also found that using ids worked, but using refs didn't.
There's probably a way to get a full two-way binding between the model and the content, but I think that would require manipulating the cursor location after each change.
<template>
<div>
<p
v-for="(value, index) in content"
:id="`content-${index}`"
:key="index"
contenteditable
@input="event => onInput(event, index)"
@keyup.delete="onRemove(index)"
/>
</div>
</template>
<script>
export default {
data() {
return {
content: [
{ value: 'paragraph 1' },
{ value: 'paragraph 2' },
{ value: 'paragraph 3' },
],
};
},
mounted() {
this.updateAllContent();
},
methods: {
onInput(event, index) {
const value = event.target.innerText;
this.content[index].value = value;
},
onRemove(index) {
if (this.content.length > 1 && this.content[index].value.length === 0) {
this.$delete(this.content, index);
this.updateAllContent();
}
},
updateAllContent() {
this.content.forEach((c, index) => {
const el = document.getElementById(`content-${index}`);
el.innerText = c.value;
});
},
},
};
</script>