At this point, you might be thinking, “Well, so what? That’s handy - but I could have used v-if just as easily.”

This example starts to shine when you realize that <component> works just like any other component, and it can be used in combination with things like v-for for iterating over a collection, or making the :is bindable to an input prop, data prop, or computed property.

What about the props and events?

Components don’t live in isolation - they need a way to communicate with the world around them. With Vue, this is done with props and events.

You can specify property and event bindings on a dynamic component the same way as any other component, and if the component that gets loaded does not need that property, Vue will not complain about unknown attributes or properties.

Let's modify our components to display a greeting. One will accept just firstName and lastName, while another will accept firstName, lastName and title.

For the events, we will add a button in DynamicOne that will emit an event called ‘upperCase’, and in DynamicTwo, a button that will emit an event ‘lowerCase’.

Putting it together, consuming the dynamic component starts to look like:

<component 
	:is="showWhich" 
	:firstName="person.firstName"
	:lastName="person.lastName"
	:title="person.title"
	@upperCase="switchCase('upperCase')"
	@lowerCase="switchCase('lowerCase')">
</component>

Not every property or event needs to be defined on the dynamic component that we are switching between.

Do you need to know the props up front?

At this point, you might be wondering, "If the components are dynamic, and not every component needs to know every possible prop - do I need to know the props up front, and declare them in the template?"

Thankfully, the answer is no. Vue provides a shortcut, where you can bind all the keys of an object to props of the component using v-bind.

This simplifies the template to:

<component 
	:is="showWhich" 
	v-bind="person"
	@upperCase="switchCase('upperCase')"
	@lowerCase="switchCase('lowerCase')">
</component>

What about Forms?

Now that we have the building blocks of Dynamic Components, we can start building on top of other Vue basics to start building a form generator.

Let's start with a basic form schema - a JSON object that describes the fields, labels, options, etc for a form. To start, we will account for:

The starting schema looks like:

schema: [{
          fieldType: "SelectList",
          name: "title",
          multi: false,
          label: "Title",
          options: ["Ms", "Mr", "Mx", "Dr", "Madam", "Lord"]
        },
        {
          fieldType: "TextInput",
          placeholder: "First Name",
          label: "First Name",
          name: "firstName"
        },
        {
          fieldType: "TextInput",
          placeholder: "Last Name",
          label: "Last Name",
          name: "lastName"
        },
        {
          fieldType: "NumberInput",
          placeholder: "Age",
          name: "age",
          label: "Age",
          minValue: 0
        }
      ]

Pretty straightforward - labels, placeholders, etc - and for a select list, a list of possible options. We will keep the component implementation for these simple for this example.

TextInput.vue - template

<div>
	<label>{{label}}</label>
	<input type="text"
         :name="name"
          placeholder="placeholder">
</div>

TextInput.vue - script

export default {
  name: 'TextInput',
  props: ['placeholder', 'label', 'name']
}

SelectList.vue - template

  <div>
    <label>{{label}}</label>
    <select :multiple="multi">
      <option v-for="option in options"
              :key="option">
        {{option}}
      </option>
    </select>
  </div>

SelectList.vue - script

export default {
  name: 'SelectList',
  props: ['multi', 'options', 'name', 'label']
}

To generate the form based on this schema, add this:

<component v-for="(field, index) in schema"
               :key="index"
               :is="field.fieldType"
               v-bind="field">
</component>

Which results in this form:

Data Binding

If a form is generated but does not bind data, is it very useful? Probably not. We currently are generating a form but have no means of binding data to it. Your first instinct might be to add a value property to the schema, and in the components use v-model like so:

<input type="text" 
    :name="name"
    v-model="value"
	  :placeholder="placeholder">

There are a few potential pitfalls with this approach, but the one that we care about most is one that Vue will give us an error/warning about:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"

found in

---> <TextInput> at src/components/v4/TextInput.vue
       <FormsDemo> at src/components/DemoFour.vue
         <App> at src/App.vue
           <Root>

While Vue does provide helpers to make two-way binding of component state easier, the framework still prefers a one-way data flow. We have tried to mutate the parent's data directly within our component, so Vue is warning us about that.

Looking a little more closely at v-model, it does not have that much magic to it, so let's break it down as described in the Vue Guide on Form Input Components.

<input v-model="something">

Is the same as:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

With the magic revealed, what we want to accomplish is:

We accomplish this by binding to the :value and emitting an @input event to notify the parent that something has changed.

Let's look at our TextInput component:

  <div>
    <label>{{label}}</label>
    <input type="text"
           :name="name"
           :value="value"
           @input="$emit('input',$event.target.value)"
           :placeholder="placeholder">
  </div>

Since the parent is responsible for providing the value, it is also responsible for handling the binding to its own component state. For this we can use v-model on the component tag:

FormGenerator.vue - template

 <component v-for="(field, index) in schema"
		:key="index"
		:is="field.fieldType"
		v-model="formData[field.name]"
		v-bind="field">
</component>

Notice how we are using v-model="formData[field.name]". We need to provide an object on the data property for this:

export default {
	data() {
    return {
      formData: {
        firstName: 'Evan'
      },
}

We can leave the object empty, or if we have some initial field values that we want to set up, we could specify them here.

Now that we have gone over generating a form, it’s starting to become apparent that this component is taking on quite a bit of responsibility. While this is not complicated code, it would be nice if the form generator itself was a reusable component.

Making the Generator Reusable

For this form generator, we will want to pass the schema to it as a prop and be able to have data-binding set up between the components.

When using the generator, the template becomes:

GeneratorDemo.vue - template

<form-generator :schema="schema" v-model="formData">
</form-generator>

This cleans up the parent component quite a bit. It only cares about FormGenerator, and not about each input type that could be used, wiring up events, etc.

Next, make a component called FormGenerator. This will pretty much be copy-pasted of the initial code with a few minor, but key tweaks:

The FormGenerator component becomes:

FormGenerator.vue - template

 <component v-for="(field, index) in schema"
               :key="index"
               :is="field.fieldType"
               :value="formData[field.name]"
               @input="updateForm(field.name, $event)"
               v-bind="field">
    </component>

FormGenerator.vue - script

import NumberInput from '@/components/v5/NumberInput'
import SelectList from '@/components/v5/SelectList'
import TextInput from '@/components/v5/TextInput'

export default {
  name: "FormGenerator",
  components: { NumberInput, SelectList, TextInput },
  props: ['schema', 'value'],
  data() {
    return {
      formData: this.value || {}
    };
  },
  methods: {
    updateForm(fieldName, value) {
      this.$set(this.formData, fieldName, value);
      this.$emit('input', this.formData)
    }
  }
};

Since the formData property does not know every possible field that we could pass in, we want to use this.$set so Vue's reactive system can keep track of any changes, and allow the FormGenerator component to keep track of its own internal state.

Now we have a basic, reusable form generator.

Using it within a component:

GeneratorDemo.vue - template

<form-generator :schema="schema" v-model="formData">
</form-generator>

GeneratorDemo.vue - script

import FormGenerator from '@/components/v5/FormGenerator'

export default {
  name: "GeneratorDemo",
  components: { FormGenerator },
  data() {
    return {
      formData: {
        firstName: 'Evan'
      },
      schema: [{ /* .... */ },
}

So now that you've seen how a form generator can leverage the basics of dynamic components in Vue to create some highly dynamic, data-driven UIs, I encourage you to play around with this example code on GitHub, or expirement on CodeSandbox. And feel free to reach out if you have any questions or want to talk shop - Twitter, Github, or Contact Rangle.io.

Sign up for our newsletter