7

I have the following parent component which has to render a list of dynamic children components:

<template>
  <div>
    <div v-for="(componentName, index) in supportedComponents" :key="index">
      <component v-bind:is="componentName"></component>
    </div>
   </div>
 </template>

<script>
const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");
export default {
  name: "parentComponent",
  components: {
    Component1,
    Component2
  },
  props: {
    supportedComponents: {
      type: Array,
      required: true
    }
  }
};
</script>

The supportedComponents property is a list of component names which I want to render in the parent conponent.

In order to use the children components in the parent I have to import them and register them.

But the only way to do this is to hard code the import paths of the components:

const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");

And then register them like this:

components: {
  Component1,
  Component2
}

I want to keep my parentComponent as generic as possible. This means I have to find a way to avoid hard coded components paths on import statements and registering. I want to inject into the parentComponent what children components it should import and render.

Is this possible in Vue? If yes, then how?

whitefang1993
  • 1,666
  • 3
  • 16
  • 27
  • Could you pass the component/s as a prop? But somewhere you have to statically import the component. `import()` is resolved during compilation time, so you need to have it during compilation time. – ssc-hrep3 Sep 24 '19 at 12:42

4 Answers4

10

You can load the components inside the created lifecycle and register them according to your array property:

<template>
    <div>
        <div v-for="(componentName, index) in supportedComponents" :key="index">
            <component :is="componentName"></component>
        </div>
    </div>
</template>

<script>
    export default {
        name: "parentComponent",
        components: {},
        props: {
            supportedComponents: {
                type: Array,
                required: true
            }
        },
        created ()  {
            for(let c=0; c<this.supportedComponents.length; c++) {
                let componentName = this.supportedComponents[c];
                this.$options.components[componentName] = () => import('./' + componentName + '.vue');
            }
        }
    };
</script>

Works pretty well

Launemax
  • 135
  • 6
  • 1
    I get `Failed to resolve async component..., Reason: Error: Cannot find module '@/components/General/TextLink.vue'`. So it cannot find the module, but that is the correct path. – whitefang1993 Sep 24 '19 at 16:47
  • Does the import work without my script? if not, its maybe an other issue. – Launemax Sep 24 '19 at 17:44
  • Just to clarify the answer by @Launemax the import statement must be a [string literal](https://stackoverflow.com/a/29168567/2437099). So ```import(componentName)``` will produce the "Failed to resolve async component..." error. Whereas import('./' + componentName + '.vue') will work – Sel Jul 12 '20 at 11:38
  • 1
    In my case I get a "Failed to mount component: template or render function not defined". The components work fine when imported normally. A console.log of this.$options.components shows that the components are registered. Path triple and quadruple-checked. Screenshot: https://dl.rowild.at/vue-component-error.png – Any ideas? – Robert Wildling May 16 '21 at 09:46
4

Here's a working code, just make sure you have some string inside your dynamic import otherwise you'll get "module not found"

       <component :is="current" />
    export default {  data () {
    return {
      componentToDisplay: null
    }
  },
  computed: {
    current () {
      if (this.componentToDisplay) {
        return () => import('@/components/notices/' + this.componentToDisplay)
      }
      return () => import('@/components/notices/LoadingNotice.vue')
    }
  },
  mounted () {
    this.componentToDisplay = 'Notice' + this.$route.query.id + '.vue'
  }
}
  • 1
    Excellent answer, I have reviewed dozens of solutions to this issue on various forums, the vue website, etc. and none of them are as clear or as good as this one. I can't imagine buildling a complex Vue site without running in to this issue. It seems to me that this is the best way to accomplish dynamically loading components within the Vue structure. There are other ways to do it - but they all feel like hacks. – Craig Nakamoto Dec 22 '21 at 16:54
2

Resolving dynamic webpack import() at runtime

You can dynamically set the path of your import() function to load different components depending on component state.

<template>
  <component :is="myComponent" />
</template>

<script>
export default {
  props: {
    component: String,
  },

  data() {
    return {
      myComponent: '',
    };
  },

  computed: {
    loader() {
      return () => import(`../components/${this.component}`);
    },
  },

  created() {
    this.loader().then(res => {
      // components can be defined as a function that returns a promise;
      this.myComponent = () => this.loader();
    },
  },
}
</script>

Note: JavaScript is compiled by your browser right before it runs. This has nothing to do with how webpack imports are resolved.

Community
  • 1
  • 1
MarcRo
  • 2,434
  • 1
  • 9
  • 24
  • this is not working, I'm getting runtime-core.esm-bundler.js?5c40:38 [Vue warn]: Invalid VNode type: undefined (undefined) – Sandeep Prasad Kushwaha Jan 27 '21 at 14:55
  • Are you using default exports? If you are using named exports you can try `() => import(...).` – MarcRo Jan 29 '21 at 21:33
  • Do you still need to register the components when you dynamically load like this? I found they did not display correctly until you throw the dynamic import into the components: {} object. also had to add defineAsyncComponent() – redfox05 Jun 04 '21 at 13:01
  • what if I have different conditions for different component? How I can render? – Bravo May 09 '22 at 18:43
  • https://stackoverflow.com/questions/72176906/how-to-import-components-dynamically-based-on-multiple-conditions-in-vue-js – Bravo May 09 '22 at 18:55
0

I think we need some plugin that can have code and every time it should load automatically. This solution is working for me.

import { App, defineAsyncComponent } from 'vue'
const componentList = ['Button', 'Card']
export const registerComponents = async (app: App): void => {
  // import.meta.globEager('../components/Base/*.vue')
  componentList.forEach(async (component) => {
    const asyncComponent = defineAsyncComponent(
      () => import(`../components/Base/${component}.vue`)
    )
    app.component(component, asyncComponent)
  })
}

you can also try glob that also work pretty well but I have checked it for this solution but check this out worth reading

Dynamic import

[Update]

I tried same with import.meta.globEage and it works only issue its little bit lazy loaded you may feel it loading slow but isn't noticeable much.

import { App, defineAsyncComponent } from 'vue'
export const registerComponents = async (app: App): void => {
Object.keys(import.meta.globEager('../components/Base/*.vue')).forEach(
    async (component) => {
    const asyncComponent = defineAsyncComponent(
        () => import(/* @vite-ignore */ component)
    )
    app.component(
        (component && component.split('/').pop()?.split('.')[0]) || '',asyncComponent
    )
    })
}
Vishvendra Singh
  • 484
  • 5
  • 19