13

Vue 3 doesn't have Vue.extend() method, so example here doesn't work: https://css-tricks.com/creating-vue-js-component-instances-programmatically/

I have tried this solution: https://jsfiddle.net/jamesbrndwgn/fsoe7cuy/

But it causes warning in console:

Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref.

enter image description here

So, what is the proper way to add components dynamically (programmatically) in Vue 3?

UploadTaskTagManager.vue

<template>
    <button @click="addTag()" type="button">Add new tag</button>
    <br>
    <div>
        <div v-for="child in children" :key="child.name">
            <component :is="child"></component>
        </div>
    </div>
</template>

<script>
    import UploadTaskTag from "./UploadTaskTag";

    export default {
        name: "UploadTaskTagManager",
        components: {UploadTaskTag},

        data() {
            return {
                children: []
            }
        },

        methods: {
            addTag() {
                this.children.push(UploadTaskTag);
            },
        }
    }
</script>

UploadTaskTag.vue

<template>
    <div>
        <select @change="onChangeTag($event)" v-model="currentTag">
            <option v-for="tag in getAllTags()" :key="tag.tag_name" :value="tag">{{tag.tag_name}}</option>
        </select>
        <input maxlength="16" class="tag-input" ref="inputField"/>
        <button @click="removeTag($event)" type="button">-</button>
        <br>
    </div>
</template>

<script>
    export default {
        name: "UploadTaskTag",

        data() {
            return {
                tags: [],
                currentTag: {}
            }
        },

        methods: {
            onChangeTag(event) {
                this.$refs.inputField.value = this.currentTag.tag_name;
            },

            getAllTags() {
                return this.$store.state.allTags;
            },

            removeTag() {
                this.$el.parentElement.removeChild(this.$el)
            },

            getValue(fieldIndex) {
                let tag = this.tags[fieldIndex];
                return tag ? tag.tag_name : "";
            },
        }
    }
</script>
0

4 Answers 4

17

Use createApp instead of Vue.extend here's a example of simple vue3 components programmatically

import Button from 'Button.vue'
import { createApp } from "vue"
// Creating the Instance
// use createApp https://v3.vuejs.org/api/global-api.html#createapp
var ComponentApp = createApp(Button)
import Button from "./Button.vue"

// inserting to dom
const wrapper = document.createElement("div")
ComponentApp.mount(wrapper)
document.body.appendChild(wrapper)

set props or slot is a little different by use h, https://v3.vuejs.org/api/global-api.html#h

import Button from 'Button.vue'
import { createApp, h } from "vue"

var ComponentApp = createApp({ 
  setup () {
    return () => h(Button, {type: "primary"}, "default slot as children")
  }
})
Sign up to request clarification or add additional context in comments.

7 Comments

Somehow it just feels wrong to use createApp() to add a small Vue component programatically. It conjures up visions of bloat and overhead for me. I have to hand it to you though, your answer does the trick.
i am facing a similar issue. I want to just create a component on the fly and append it to the DOM in Vue 3 and it seems unnecessarily hard. Why create a new instance of Vue? I do not get it. There's no simpler way?
The only issue that I have with this solution is that you lose any sort of scope that your main application has... e.g. if you have global vars or items that are provided, etc. Otherwise, this answer is exactly it.
Update: if you want to add in various properties from your main application, you can do it manually by accessing the _context field on your main app. Example: newApp.config.globalProperties = mainApp._context.config.globalProperties;
Great find, @NerdSoup! I'd imagine if anyone's creating a lot of components like this that they'd wrap it in a common function, like createComponent, so you don't have to remember to do it every time.
|
7

If I understood you correctly, then the solution will be like this

You can learn more about 'defineAsyncComponent' on the official website https://v3.vuejs.org/api/global-api.html#defineasynccomponent

<template>
    <button @click="addTag()" type="button">Add new tag</button>
    <br>
    <div>
        <div v-for="child in children" :key="child.name">
            <component :is="child"></component>
        </div>
    </div>
</template>

<script>

import { defineAsyncComponent, defineComponent, ref } from "vue"

export default defineComponent({
components: {     
      UploadTaskTag: defineAsyncComponent(() => import("./UploadTaskTag"))
    },
 setup() {
      const children = ref([])

      const addTag = () => {
        children.value.push('UploadTaskTag')
        console.log(children.value)
      }
return {
        addTag,
        children        
      }
    },
  })
</script>

Comments

2

I recently had a similar problem where I wanted to bulk import components from a directory where I didn't know the names of the files ahead of time. This is because we have different components in different environments.

Using Vite allows you to do glob imports, so I combined this with the vue3 "defineAsyncComponent" as you can see in @Oleksii-Zelenko answer.

Doing it this way, you can avoid the global registration problem mentioned in other answers.

The following snippet is how I solved the problem:

<script setup lang="ts">
  // Any typical vue3 setup code can go here
  defineProps({
    myProp: {
      type: Object,
      default() {
        return {};
      },
    },
  });
</script>
<script lang="ts">
  import { defineAsyncComponent, defineComponent } from "vue"

  const widgets = import.meta.glob('./widgets/*.vue', { import: 'default' });
  const components = {};
  Object.keys(widgets).forEach((k) => {
    // convert the file name to a friendlier format
    const [fileName] = k.split('/').reverse()
    const [componentName] = fileName.split('.');
    // assign the async component to the friendlier name
    components[componentName] = defineAsyncComponent(widgets[k]);
  });
  export default defineComponent({
    components
  });
</script>
<template>
  <component
    v-for="component in components"
    :is="component"
    :some-prop="myProp"
  />
</template>

Comments

0

I needed to do such a thing to add a Component to a contenteditable div.

To do so, I used the Teleport component, bound to and array containing the name of the wished componend, the div where it'll be mount and optionaly the props.

Here is a generic code sample.

Image = {
  template: document.getElementById('image-template'),
  props: ['src']
}

const app = Vue.createApp({
  template: document.getElementById('app-template'),
  components: {
    Image
  },
  data() {
    return {
      dynamicComponents: []
    }
  },
  methods: {
    async addImg() {
      let span = document.createElement("span");
      document.getElementById('main').appendChild(span);
      await this.$nextTick();
      this.$appendComponent(
        'Image',
        span, {
          src: 'https://images-na.ssl-images-amazon.com/images/I/81-jLAuCQzL.png'
        }, {
          click: () => {
            alert('Clicked')
          }
        });
    }
  }
});

app.config.globalProperties.$appendComponent = function(name, target, props, listeners = {}) {
  this.$root.dynamicComponents.push({
    name,
    target,
    props,
    listeners
  });
}

app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>

<div id="app">
</div>

<template id="app-template">
  <Teleport v-for="{name, target, props, listeners},i in dynamicComponents" :key="i" :to="target">
    <Component :is="name" v-bind="props" v-model="props.modelValue" v-on="listeners"/>
  </Teleport>
  
  <button @click="addImg">Add Image</button>
  
  <div id="main">
    Coucou
  </div>
</template>

<template id="image-template">
  <img :src="src" />
</template>

You can now programatically create DOM nodes anywhere, then adding an entry to the $root.dynamicComponents array will mount a component in it. Context consistant and no bloating.

Comments

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.