I'm trying to do an editable component with Vue 2. It supposed to use the contenteditable
attribute in any tag, replacing a normal input. I want to give it a placeholder functionality in order to show a value when none is provided by the user, but I can't seem to get it working.
I'm watching the current value of the component and setting data.isEmpty
to true
when no user content is present. The component should then show the placeholder value, but currently it shows nothing.
If I console.log
the result of the render
method, it will show the placeholder child node was instantiated correctly, but for some reason it just won't show on the final HTML.
Here's a JSFiddle: https://jsfiddle.net/dy27fa8t/
And an embedded snippet for those who prefer it:
Vue.component('editable-content', {
props: {
initial: {
type: String
},
placeholder: {
type: String,
required: false
}
},
data() {
return {
value: this.initial,
isEmpty: this.initial === ''
}
},
render: function(createElement) {
const self = this
return createElement(
'div', {
attrs: {
contenteditable: true
},
on: {
input: function(event) {
self.value = event.target.innerHTML
self.$emit('edited', event.target.value)
}
}
},
this.isEmpty ? this.placeholder : this.value
)
},
watch: {
value(to, from) {
this.isEmpty = to === ''
}
}
})
new Vue({
el: '#app',
components: [
'editable-content'
]
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.min.js"></script>
<div id="app">
<editable-content initial="Initial value" placeholder="Placeholder" />
</div>
The vue template is much more readable and easier to understand than jsx functions. In short, it's better to use the vue template because it's easier to implement dynamic pages with it (and other things). Also, at this point it's not a very good idea to keep sending html code through methods.
A render function returns a virtual DOM node, commonly named VNode in the Vue ecosystem, which is an interface that allows Vue to write these objects in your browser DOM. They contain all the information necessary to work with Vue.
STEP 01: In the parent component, create a variable called name inside the data() model. STEP 02: Bind the name property with a value myName to the child component when rendering it on the parent template. The value of the name property is passed to myName and you can access it inside the child component.
The best way to force Vue to re-render a component is to set a :key on the component. When you need the component to be re-rendered, you just change the value of the key and Vue will re-render the component.
Apparently rendering a contenteditable
doesn't work in the intuitive way. Instead, set the innerHTML
directly with the placeholder when the content is empty. Then on keydown
(before the input event), if the content is currently marked empty, remove the placeholder. On keyup
(after the input event), if the div still has no content, mark it empty again (this is so things like shift key don't clear the placeholder).
I took the liberty of making it v-model
compatible and styling the placeholder.
Vue.component('editable-content', {
props: {
value: {
type: String
},
placeholder: {
type: String,
required: false
}
},
data() {
return {
isEmpty: this.value === ''
};
},
methods: {
setEmpty() {
this.$el.innerHTML = `<div contenteditable="false" class="placeholder">${this.placeholder}</div>`;
this.isEmpty = true;
},
clearEmpty() {
this.$el.innerHTML = '';
this.isEmpty = false;
}
},
mounted() {
if (this.$el.innerHTML === '') {
this.setEmpty();
}
},
watch: {
value(newValue) {
if (newValue === '') {
this.setEmpty();
}
}
},
render: function(createElement) {
return createElement(
'div', {
attrs: {
contenteditable: true
},
on: {
keydown: () => {
if (this.isEmpty) {
this.clearEmpty();
}
},
input: (event) => {
this.$emit('input', event.target.textContent);
},
keyup: () => {
if (this.$el.innerHTML === '') {
this.setEmpty();
}
}
}
},
this.value
)
}
});
new Vue({
el: '#app',
data: {
startingBlank: '',
editedValue: 'initial value'
},
components: [
'editable-content'
]
})
.placeholder {
color: rgba(0,0,0, 0.5);
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.min.js"></script>
<div id="app">
<editable-content v-model="startingBlank" placeholder="Placeholder"></editable-content>
<editable-content v-model="editedValue" placeholder="Placeholder"></editable-content>
</div>
In the end I settled for a mixed JS and CSS solution using the :empty
pseudo-class. A Vue-only workaround just seemed too unwieldy, so this felt like a healthy compromise. I don't even feel the need to keep track of the value
anymore.
Worth noting that with single-file components I can use scoped CSS so it's even better as the CSS is essential to the components core functionality.
Vue.component('editable-content', {
props: {
initial: {
type: String
},
placeholder: {
type: String,
required: false
}
},
data() {
return {
value: this.initial
}
},
render: function(createElement) {
const self = this
return createElement(
'div', {
attrs: {
contenteditable: true,
'data-placeholder': this.placeholder
},
on: {
input: function(event) {
self.$emit('edited', event.target.value)
}
}
},
this.value
)
}
})
new Vue({
el: '#app',
components: [
'editable-content'
]
})
[data-placeholder]:empty::after {
content: attr(data-placeholder);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.min.js"></script>
<div id="app">
<editable-content initial="Initial value" placeholder="Placeholder" />
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With