Subscribe on changes!

Capability to add an existing DOM element in Vue component

avatar
Jul 25th 2022

What problem does this feature solve?

This feature request proposes a new built-in special element to add existing DOM element in Vue component

DOM interafce Element provides prototype methods insertAdjacentText, insertAdjacentHTML and insertAdjacentElement to insert text, html and element respectively. Currently Vue has {{xxx}}, v-html="xxx" directives to declare text node value and inner html, but no built-in directive to insert an existing element.

Assuming I have a canvas element which is generated by a third-party lib. To add existing canvas element in Vue component, one approach is to manipulate the DOM, with code like this.$el.querySelector('.xxx').insertAdacentElement('afterend', el) or this.$refs.xxx.replceWith(el), but it's not a good pratice unless one have no other way, see TIPS FOR VUE DEVELOPERS: Avoid directly manipulating the DOM and elegantly-inject-pre-rendered-element-into-vue-template, another approach is to convert element to HTML and use the v-html directive, but it isn't always practicable, how about the graphics of canvas element?

What does the proposed API look like?

Vue currently provides two built-in special elements <slot> and <component>.

The <slot> element is a slot outlet that indicates where the parent-provided slot content should be rendered.

The <component> element is a "meta component" for rendering dynamic components or elements.

This feature request proposes a new built-in special element, which is

  • something like <slot> but source content may come from component itself rather than only from parent, and the type is Element rather than VNode.
  • something like <component> but we would to pass an Element instance (similar to DOM method HTMLSlotElement.prototype.assign), rather than passing a tag name / a component name / a component definition.
  • something like {{xxx}} and v-html="xxx", but for DOM element.

e.g. <element-placeholder> as defined below

// ElementPlaceholder.js
export default {
  props: {
    el: Element,
  },
  render(h) {
    return [];
  },
  mounted(){
    let el = this.$el;
    let newEl = this.el;
    if(newEl){
      el.replaceWith(newEl);
      this.currentEl = newEl;
    }else{
      this.currentEl = this.$el;
    }
  },
  watch: {
    el: function (newEl, oldEl) {
      let el = this.currentEl;
      if(newEl){
        el.replaceWith(newEl);
        this.currentEl = newEl;
      }else{
        let originalEl = this.$el;
        el.replaceWith(originalEl);
        this.currentEl = originalEl;
      }
    },
  }
};

Example usage:

// App.js
import ElementPlaceholder from './ElementPlaceholder.js';

let App = {
  components: {
    'element-placeholder': ElementPlaceholder,
  },
  template: /*html*/ `<div id="app">
  <element-placeholder :el="canvas"></element-placeholder>
  <button @click="init" :disabled="canvas!=null">Init</button>
  <button @click="reset" :disabled="canvas==null">Reset</button>
</div>`,
  data(){
    return {
      canvas: null,
    };
  },
  created(){
    this.init();
  },
  methods: {
    init(){
      this.canvas = Object.assign(document.createElement('canvas'), {
        width: 64,
        height: 64
      });
    },
    reset(){
      this.canvas = null;
    },
  }
};

There are other concerns, e.g. would <element-placeholder> work well with v-for / v-if?

This is just a basic idea, I'm not a Vue expert.