使用渲染功能时 Vue 组件不显示子文本节点

Vue component not showing child text node when using render function

我想用 Vue 2 做一个可编辑的组件。它应该在任何标签中使用 contenteditable 属性,替换普通输入。我想给它一个占位符功能,以便在用户提供 none 时显示一个值,但我似乎无法让它工作。

我正在观察组件的当前值,并在没有用户内容时将 data.isEmpty 设置为 true。该组件随后应显示占位符值,但目前它什么都不显示。

如果我 console.log render 方法的结果,它将显示占位符子节点已正确实例化,但由于某些原因它不会显示在最终 HTML.

这是一个 JSFiddle:https://jsfiddle.net/dy27fa8t/

还有为喜欢它的人提供的嵌入式代码段:

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>

如果您不将 initial 属性传递给组件,它将是未定义的。所以你应该做的检查是看是否未定义:

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

显然渲染 contenteditable 不符合直观的方式。相反,当内容为空时,直接使用占位符设置 innerHTML。然后在 keydown(输入事件之前),如果内容当前标记为空,则移除占位符。在 keyup 上(输入事件后),如果 div 仍然没有内容,再次将其标记为空(这样 shift 键不会清除占位符)。

我冒昧地使其 v-model 兼容并对占位符进行样式设置。

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>

最后我选择了混合 JS 和 CSS 解决方案,使用 :empty 伪 class。仅 Vue 的解决方法似乎太笨重,所以这感觉像是一个健康的妥协。我什至觉得没有必要再跟踪 value

值得注意的是,对于单文件组件,我可以使用范围 CSS,因此它更好,因为 CSS 对组件的核心功能至关重要。

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>