Dynamic components with dynamic imports

Hello All,

I have a “widget-container”-component which has “widget”-components in it. The widget components can be of various types. This widget component looks like this:


<template>
	<div class="card">
		<component v-if="widget.type" :is="widgetView" :widget="widget"></component>
	</div>
</template>

<script>
import WidgetOne from './widgets/WidgetOne.vue'
import WidgetTwo from './widgets/WidgetTwo.vue'
import WidgetThree from './widgets/WidgetThree.vue'
import WidgetFour from './widgets/WidgetFour.vue'
import WidgetFive from './widgets/WidgetFive.vue'
import WidgetSix from './widgets/WidgetSix.vue'
import WidgetSeven from './widgets/WidgetSeven.vue'
import WidgetEight from './widgets/WidgetEight.vue'

export default{
	components: {
		WidgetOne,
		WidgetTwo,
		WidgetThree,
		WidgetFour,
		WidgetFive,
		WidgetSix,
		WidgetSeven,
		WidgetEight
	},
	computed: {
		widgetView(){
			return "Widget" + this.widget.type
		}
	}
}

It’s expected that more widgets will be added in the future and we don’t want to import every possible widget type like this. Is there a way that i can import only the component which corresponds to the widgettype?

Ow and i do not want to import them in a seperate file to later import that file

Thanks

2 Likes

Yes, with async components.

const WidgetOne = import('./widgets/WidgetOne.vue')
const WidgetTwo = import('./widgets/WidgetTwo.vue')
const WidgetThree = import('./widgets/WidgetThree.vue')

export default{
  components: {
    WidgetOne,
    WidgetTwo,
    WidgetThree,
  },
  computed: {
    widgetView(){
      return "Widget" + this.widget.type
    }
  }
}
1 Like

Hey @woodberry,

Thanks for your reply.

Somehow the const solution did not work, although i see many people are succesfull using it. The link was very usefull.
I managed to do the following:

function loadWidget(widget){
	widget += '.vue'
	console.log("Loaded " + widget);
	return System.import('./widgets/' + widget); 
}

export default{
	components: {
		WidgetOne: () => loadWidget('WidgetOne'),
		WidgetTwo: () => loadWidget('WidgetTwo'),
		WidgetThree: () => loadWidget('WidgetThree')
	}
}

This works for me because all widgets are in the same folder, i had to add the .vue-part before the System.import('...') because otherwise webpack didn’t undertand what to include.

Though when I look at the code I am not yet satisfied; i see every widgetname twice!!!

Is there a way so that i can do something like this:

function importWidgets(){
	for(component of components){
		if component.startsWith("Widget"){
			loadWidget(component)
		}
	}
}

export default{
	components: {
		'WidgetOne', //preferably without the ''
		'WidgetTwo',
		'WidgetThree'
	}
}

No, there isn’t. You will have to learn to live with this horrible double-occurence of the name.

It takes time, but in believe in you :wink:

1 Like

@LinusBorg is the solution below good? Probably not.
Does it work? Like clockwork + we lost the double names :stuck_out_tongue_winking_eye:

function loadWidget(){
	console.log("Loaded " + widgetType);
	return System.import('./widgets/' + widgetType); 
}

let widgetType

export default{
	components: {
		OneWidget: () => loadWidget(),
		TwoWidget: () => loadWidget(),
		ThreeWidget: () => loadWidget()
	},
	created() {
		axios.get(`http://localhost:3000/widgets/` + this.widgetId + '')
		.then(response => {
			this.widget = response.data
			widgetType = this.widget.type.charAt(0).toUpperCase() + this.widget.type.slice(1) + 'Widget.vue';
		})
		.catch(e => {
			this.errors.push(e)
		})
	}
}

The widgetType is determined by a value in the database.

I also tried something along the lines of:

export default{
	components: {
		AnyWidget: () => loadWidget()
	}
}

This would only work for 1 type of widget and would load all widgets like the first because of the caching, which of course is a good thing.

function loadWidget(widget){
	return {
  		[widget]: () => System.import(`/widgets/${widget}.vue`)
	}
}

export default{
	components: {
		...loadWidget('WidgetOne'),
		...loadWidget('WidgetTwo'),
		...loadWidget('WidgetThree')
	}
}

or

function loadWidgets(widgets){
	return widgets.reduce((current, widget) => ({
		...current,
  		[widget]: () => System.import(`/widgets/${widget}.vue`)
	}), {})
}

export default{
	components: loadWidgets(['WidgetOne', 'WidgetTwo', 'WidgetThree'])
}
3 Likes

I just ran into this same thing, also specifically with widgets. Here’s what I ended up doing:

<template>
	<div class="card">
		<component v-bind:is="componentFile"></component>
	</div>
</template>
<script>
export default{
	props: {
		componentName: {
			type: String,
			required: true
		}
	}
	computed: {
		componentFile() {
			return () => import(`./widgets/${this.componentName}.vue`);
		}
	}
}
</script>
8 Likes

Thank you for the response, worked like a charm for me.

I am not sure if I am understanding exactly what is happening here…you need to define a prop for each component name? Where does the list of components come from and how is it passed to the computed value?

My situation is this:

  1. The list of widgets I want to load on a page is stored in vuex
  2. Using this list, I want to import only the widgets from this list
  3. Then iterating over this list (v-for), I will define my components in the template

So I need to define imports and components at runtime, before looping/displaying my template. Any ideas how I can make this work (or if this is even possible)? Thanks!

I cannot believe how simple and effective this is. Solved a huge problem for me - really appreciate your post.

@arleonard54 Thank you your solution was just what I needed, except I ran into an issue where the computed property didn’t update if the variable in the import path was bound from Vuex. Here’s my adjustment if others use mapState instead of props

import {mapState} from 'vuex'

export default {
  computed: {
   ...mapState('componentName'),
   componentFile() {
    return this.importComponent(this.componentName)
   }
  },
  methods: {
   importComponent(path) {
    return () => import(`./widgets/${path}.vue`)
   }
  }
}

As you can see, the main difference is what triggers Vue’s watchers to update the computed property. Although it’s a fat arrow and does not create a new scope, it seems Vue does not watch for changes on any this properties inside the body of a returned function. My change just adds a intermediary step to get Vue to watch for changes to that variable and rerun the import function.