Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vue component not showing child text node when using render function

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>
like image 770
Tomas Buteler Avatar asked May 01 '17 14:05

Tomas Buteler


People also ask

Should you use JSX with Vue?

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.

For what purpose is the Render () function used in Vue?

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.

How do I send data to child component 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.

How do you force a child component to Rerender Vue?

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.


2 Answers

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>
like image 189
Roy J Avatar answered Oct 22 '22 11:10

Roy J


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>
like image 40
Tomas Buteler Avatar answered Oct 22 '22 12:10

Tomas Buteler