6

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>

4
  • When you say, "when none is provided by the user" do you mean when there is no initial attribute bound on the component? Commented May 1, 2017 at 14:53
  • Well, that too. But I'm mostly referring to the user going to edit the element and deleting all characters. I'm basically trying to reproduce the placeholder on an HTML input, so when no value is set the placeholder shows up. Commented May 1, 2017 at 14:58
  • 1
    It has something to do with it re-using the DOM element and the DOM element having focus. What I mean is, if you change the value without it having focus (like with setTimeout) it sets the placeholder value. Likewise, if you add a unique key on each render, it will set the placeholder, but you lose focus on the DOM element. Commented May 1, 2017 at 15:25
  • I've tested this and you're right. Bummer. Sure hope there's a workaround, because losing focus on every edit sort of defeats the purpose of having this component... Commented May 1, 2017 at 15:33

3 Answers 3

2

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>

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, this truly works as I originally intended. On the other hand, it feels a bit overblown for what was supposed to be just a quick-fire substitute to ordinary HTML. I've posted my own answer now for the sake of comparison, since it's a slightly different approach.
2

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>

1 Comment

I think the CSS approach is the right choice. Wish I'd thought of it.
1

If you don't pass the initial prop into the component it will be undefined. So the check you should make is to see if is undefined:

data() {
    return {
      value: this.initial,
      isEmpty: typeof this.initial === 'undefined'
    }
  },

2 Comments

This doesn't solve his problem; just the problem I asked about in the comments.
It's a fair point and I need to fix it, but @BertEvans is right in that it doesn't solve the actual problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.