DEV Community

Cover image for Dynamic components, a Vue.js superpower
Reynold Osei Adade
Reynold Osei Adade

Posted on

Dynamic components, a Vue.js superpower

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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 {}
      }
    },
Enter fullscreen mode Exit fullscreen mode

Personal.vue

//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>

Enter fullscreen mode Exit fullscreen mode

Work.vue

// 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>


Enter fullscreen mode Exit fullscreen mode

ViewSubmit.vue


// 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>


Enter fullscreen mode Exit fullscreen mode

I hope this help open up some new and interesting possibilities
Github for this code is attached below

Thanks.😉

Top comments (0)