Dynamic components, a Vue.js superpower
Reynold Osei Adade
Posted on October 27, 2021
If you have ever encountered tabs in your code, then its likely there is one of 3 things that have been implemented
- v-if and v-else
- Nested routes
- Dynamic components
If you are ever in the situation where you have to implement anything that requires you to conditionally show one component at a time, if you are thinking of using v-if and v-else
then please consider using dynamic components.
Dynamic components if used well require less code in your template and finer control over the components being displayed, all in all keeping the conditions out of your html and into your JavaScript where it is easier to maintain.
Concepts are best explained in code, so lets start with the code below which is basically a project that illustrates one of the use cases for dynamic components, the code consists of 4 component
- index.vue which is just the wrapper for the other components
- Personal.vue which contains a form for collecting personal details
- Work.vue which contains a form for collecting work details
- ViewSubmit.vue for displaying the gathered data
The main point of focus should be
-
<component />
-
<keep-alive></keep-alive>
<component />
requires a single prop, is
which is just the name of the component you want to display as a string, other than that is behaves just like any other custom component that you build, it takes custom props like any other component
To be able to keep the component in the dom with all its states you can wrap it with keep-alive
this ensures that if the component is switched all its data is kept just as it is.
So if you have ever wondered how to split forms into multiple sections but still keep your data, this is one way to do it.
//index.vue
<template>
<div class="w-full p-10 flex flex-col justify-center items-center">
<div class="p-2 w-1/2">
<div class="flex">
<button
v-for="button in buttons"
:key="button.name"
class="flex-1 p-2 border"
:class="{
'bg-white text-blue-500': button.component !== currentComponent,
'bg-blue-500 text-white': button.component === currentComponent,
}"
@click.prevent="setCurrentComponent(button.component)"
>
{{ button.name }}
</button>
</div>
<div class="">
// keep state of components
<keep-alive>
<Component
:is="currentComponent"
:personal="personal"
:work="work"
:save-personal-details="savePersonalDetails"
:save-work-details="saveWorkDetails"
:set-current-component="setCurrentComponent"
/>
</keep-alive>
</div>
</div>
</div>
</template>
<script>
import Personal from '@/components/Personal.vue'
export default {
components: {
Personal,
Work: () => import('~/components/Work.vue'),
ViewSubmit: () => import('~/components/ViewSubmit.vue'),
},
data() {
return {
currentComponent: 'Personal',
buttons: [
{ name: 'Personal Details', component: 'Personal' },
{ name: 'Work Details', component: 'Work' },
{ name: 'View and Submit', component: 'ViewSubmit' },
],
personal: {},
work: {},
}
},
methods: {
setCurrentComponent(component) {
this.currentComponent = component
},
saveWorkDetails(form) {
this.work = form
this.currentComponent = 'ViewSubmit'
},
savePersonalDetails(form) {
this.personal = form
this.currentComponent = 'Work'
},
},
}
</script>
As an extra bonus for using dynamic components you get to have the all your props available to all components all the time, this in a way saves you a lot of repetitions. But if you are like me and you want to fine tune which props are available to which components, especially in the scenario where you want the same prop names to contain different data you can use this nifty little trick I picked up
//index.vue optional
<template>
<div class="w-full p-10 flex flex-col justify-center items-center">
<div class="p-2 w-1/2">
<div class="flex">
<button
v-for="button in buttons"
:key="button.name"
class="flex-1 p-2 border"
:class="{
'bg-white text-blue-500': button.component !== currentComponent,
'bg-blue-500 text-white': button.component === currentComponent,
}"
@click.prevent="setCurrentComponent(button.component)"
>
{{ button.name }}
</button>
</div>
<div class="">
<!-- keep state of components -->
<keep-alive>
<Component :is="currentComponent" v-bind="changePropsByComponent" />
</keep-alive>
</div>
</div>
</div>
</template>
<script>
import Personal from '@/components/Personal.vue'
export default {
components: {
Personal,
Work: () => import('~/components/Work.vue'),
ViewSubmit: () => import('~/components/ViewSubmit.vue'),
},
data() {
return {
currentComponent: 'Personal',
buttons: [
{ name: 'Personal Details', component: 'Personal' },
{ name: 'Work Details', component: 'Work' },
{ name: 'View and Submit', component: 'ViewSubmit' },
],
personal: {},
work: {},
}
},
// focus here
computed: {
changePropsByComponent() {
switch (this.currentComponent) {
case 'Personal':
return {
savePersonalDetails: this.savePersonalDetails,
}
case 'Work':
return {
saveWorkDetails: this.saveWorkDetails,
setCurrentComponent: this.setCurrentComponent,
}
case 'ViewSubmit':
return {
personal: this.personal,
work: this.work,
}
default:
return {}
}
},
},
// end
methods: {
setCurrentComponent(component) {
this.currentComponent = component
},
saveWorkDetails(form) {
this.work = form
this.currentComponent = 'ViewSubmit'
},
savePersonalDetails(form) {
this.personal = form
this.currentComponent = 'Work'
},
},
}
</script>
What the code below does is to return props based on the current component being shown. You can use this method to gain finer control over your props
//computed value
changePropsByComponent() {
switch (this.currentComponent) {
case 'Personal':
return {
savePersonalDetails: this.savePersonalDetails,
}
case 'Work':
return {
saveWorkDetails: this.saveWorkDetails,
setCurrentComponent: this.setCurrentComponent,
}
case 'ViewSubmit':
return {
personal: this.personal,
work: this.work,
}
default:
return {}
}
},
//personal.vue
<template>
<div class="w-full p-20">
<form @submit.prevent="savePersonalDetails(form)">
<div class="grid grid-cols-2">
<div class="p-2">
<label for="name">Name</label>
<input
id="name"
v-model="form.name"
type="text"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="date">DOB</label>
<input
id="date"
v-model="form.date"
type="date"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="amount">ID Number</label>
<input
id="id"
v-model="form.id"
type="text"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="email">Email</label>
<input
id="email"
v-model="form.email"
type="email"
class="p-2 rounded border w-full"
/>
</div>
</div>
<div class="p-2 m-2">
<button class="p-2 w-full bg-green-500 text-white" type="submit">
Save and Continue <i class="fas fa-arrow-right"></i>
</button>
</div>
</form>
</div>
</template>
<script>
export default {
props: {
savePersonalDetails: {
type: Function,
default: () => {},
},
},
data() {
return {
form: {
name: '',
date: '',
email: '',
id: '',
},
}
},
}
</script>
// Work.vue
<template>
<div class="w-full">
<div class="w-full p-20">
<form @submit.prevent="saveWorkDetails(form)">
<div class="grid grid-cols-2">
<div class="p-2">
<label for="company_name">Company Name</label>
<input
id="company_name"
v-model="form.companyName"
type="text"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="role">Role</label>
<input
id="role"
v-model="form.role"
type="text"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="work_adresss">Address</label>
<input
id="work_adresss"
v-model="form.address"
type="text"
class="p-2 rounded border w-full"
/>
</div>
<div class="p-2">
<label for="work_email">Email</label>
<input
id="work_email"
v-model="form.email"
type="email"
class="p-2 rounded border w-full"
/>
</div>
</div>
<div class="p-2 m-2 flex">
<button
class="p-2 w-full bg-gray-400 text-white m-1"
type="button"
@click.prevent="setCurrentComponent('Personal')"
>
Previous <i class="fas fa-arrow-left"></i>
</button>
<button class="p-2 w-full bg-green-500 text-white m-1" type="submit">
Save and continue <i class="fas fa-arrow-right"></i>
</button>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
props: {
saveWorkDetails: {
type: Function,
default: () => {},
},
setCurrentComponent: {
type: Function,
default: () => {},
},
},
data() {
return {
form: {
companyName: '',
role: '',
email: '',
address: '',
},
}
},
}
</script>
<style></style>
// ViewSubmit.vue
<template>
<div class="w-full">
<div>
<div class="p-2 border rounded m-1">Name: {{ personal.name }}</div>
<div class="p-2 border rounded m-1">DOB: {{ personal.date }}</div>
<div class="p-2 border rounded m-1">Email: {{ personal.email }}</div>
<div class="p-2 border rounded m-1">ID: {{ personal.id }}</div>
</div>
<div>
<div class="p-2 border rounded m-1">
Organization: {{ work.companyName }}
</div>
<div class="p-2 border rounded m-1">Role: {{ work.role }}</div>
<div class="p-2 border rounded m-1">Email: {{ work.email }}</div>
<div class="p-2 border rounded m-1">Address: {{ work.address }}</div>
</div>
</div>
</template>
<script>
export default {
props: {
work: {
type: Object,
default: () => {},
},
personal: {
type: Object,
default: () => {},
},
},
}
</script>
I hope this help open up some new and interesting possibilities
Github for this code is attached below
Thanks.😉
Posted on October 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.