Building Complex Forms In Vue

About The Author

Olufunke is a Frontend Engineer who enjoys crafting software and writing about her engineering experiences. More about Olufunke ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Chances are, we will have to build a complex form at least once in our software engineering journey. This article goes over creating a complex form that can be progressively enhanced using some Vue features like the v-for and the v-model. It also gives a refresher on some basic Vue core features that will come in handy when building out the complex form in your day-to-day Vue usage.

More often than not, web engineers always have causes to build out forms, from simple to complex. It is also a familiar pain in the shoe for engineers how fast codebases get incredibly messy and incongruously lengthy when building large and complex forms. Thus begging the question, “How can this be optimized?”.

Consider a business scenario where we need to build a waitlist that captures the name and email. This scenario only requires two/three input fields, as the case may be, and could be added swiftly with little to no hassle. Now, let us consider a different business scenario where users need to fill out a form with ten input fields in 5 sections. Writing 50 input fields isn’t just a tiring job for the Engineer but also a waste of great technical time. More so, it goes against the infamous “Don’t Repeat Yourself” (DRY) principle.

In this article, we will focus on learning to use the Vue components, the v-model directive, and the Vue props to build complex forms in Vue.

The v-model Directive In Vue

Vue has several unique HTML attributes called directives, which are prefixed with the v-. These directives perform different functions, from rendering data in the DOM to manipulating data.

The v-model is one such directive, and it is responsible for two-way data binding between the form input value and the value stored in the data property. The v-model works with any input element, such as the input or the select elements. Under the hood, it combines the inputted input value and the corresponding change event listener like the following:

<!-- Input element -->
<input v-model="inputValue" type="text">

<!-- Select element -->
<select v-model="selectedValue">
  <option value="">Please select the right option</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

The input event is used for the <input type= "text"> element. Likewise, for the <select> … </select>, <input type= "checkbox"> and <input type= "radio">, the v-model will, in turn, match the values to a change event.

Components In Vue

Reusability is one of the core principles of Software Engineering, emphasizing on using existing software features or assets in a software project for reasons ranging from minimizing development time to saving cost.

One of the ways we observe reusability in Vue is through the use of components. Vue components are reusable and modular interfaces with their own logic and custom content. Even though they can be nested within each other just as a regular HTML element, they can also work in isolation.

Vue components can be built in two ways as follows:

  • Without the build step,
  • With the build step.

Without The Build Step

Vue components can be created without using the Vue Command Line Interface (CLI). This component creation method defines a JavaScript object in a Vue instance options property. In the code block below, we inlined a JavaScript string that Vue parses on the fly.

template: `
  <p> Vue component without the build step </p>
  `

With The Build Step

Creating components using the build step involves using Vite — a blazingly fast, lightweight build tool. Using the build step to create a Vue component makes a Single File Component (SFC), as it can cater to the file’s logic, content, and styling.

<template>
  <p> Vue component with the build step </p>
</template>

In the above code, we have the <p> tag within the HTML <template> tag, which gets rendered when we use a build step for the application.

Registering Vue Components

Creating a Vue component is the first step of reusability and modularity in Vue. Next is the registration and actual usage of the created Vue component.

Vue components allow the nesting of components within components and, even more, the nesting of components within a global or parent component.

Let’s consider that we stored the component we created using the build step in a BuildStep.vue file. To make this component available for usage, we will import it into another Vue component or a .vue, such as the root entry file. After importing this component, we can then register the component name in the components option property, thus making the component available as an HTML tag. While this HTML tag will have a custom name, the Vue engine will parse them as valid HTML and render them successfully in the browser.

<!-- App.vue -->
<template>
  <div>
    <BuildStep />
  </div>
</template>

<script>
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
</script>

From the above, we imported the BuildStep.vue component into the App.vue file, registered it in the components option property, and then declared it within our HTML template as <BuildStep />.

Vue Props

Vue props, otherwise known as properties, are custom-made attributes used on a component for passing data from the parent component to the child component(s). A case where props can come in handy is when we need a component with different content but a constant visual layout, considering a component can have as many props as possible.

The Vue prop has a one-way data flow, i.e., from the parent to the child component. Thus, the parent component owns the data, and the child component cannot modify the data. Instead, the child component can emit events that the parent component can record.

Props Declaration In Vue

Let us consider the code block below:

<template>
  <p> Vue component {{ buildType }} the build step</p>
</template>

<script>
export default {
  props: {
    buildType: {
      type: String
    }
  }
}
</script>

We updated the HTML template with the interpolated buildType, which will get executed and replaced with the value of the props that will be passed down from the parent component.

We also added a props tag in the props option property to listen to the props change and update the template accordingly. Within this props option property, we declared the name of the props, which matches what we have in the <template> tag, and also added the props type.

The props type, which can be Strings, Numbers, Arrays, Boolean, or Objects, acts as a rule or check to determine what our component will receive.

In the example above, we added a type of String; we will get an error if we try to pass in any other kind of value like a Boolean or Object.

Passing Props In Vue

To wrap this up, we will update the parent file, i.e., the App.vue, and pass the props accordingly.

<!-- App.vue -->
<template>
  <div>
    <BuildStep buildType="with"/>
  </div>
</template>

<script>
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
</script>

Now, when the build step component gets rendered, we will see something like the following:

Vue component with the build step

With props, we needn’t create a new component from scratch to display whether a component has a build step or not. We can again declare the <BuildStep /> component and add the relevant build type.

<!-- App..vue -->
<template>
  <div>
    <BuildStep buildType="without"/>
  </div>
</template>

Likewise, just as for the build step, when the component gets rendered, we will have the following view:

Vue component without the build step

Event Handling In Vue

Vue has many directives, which include the v-on. The v-on is responsible for listening and handling DOM events to act when triggered. The v-on directive can also be written as the @ symbol to reduce verbosity.

<button @click="checkBuildType"> Check build type </button>

The button tag in the above code block has a click event attached to a checkBuildType method. When this button gets clicked, it facilitates executing a function that checks for the build type of the component.

Event Modifiers

The v-on directive has several event modifiers that add unique attributes to the v-on event handler. These event modifiers start with a dot and are found right after the event modifier name.

<form @submit.prevent="submitData">
 ...
<!-- This enables a form to be submitted while preventing the page from being reloaded. -->
</form>

Key Modifiers

Key modifiers help us listen to keyboard events, such as enter, and page-up on the fly. Key modifiers are bound to the v-on directive like v-on:eventname.keymodifiername, where the eventname could be keyup and the modifiername as enter.

<input @keyup.enter="checkInput">

The key modifiers also offer flexibility but allow multiple key name chaining.

<input @keyup.ctrl.enter="checkInput">

Here the key names will listen for both the ctrl and the enter keyboard events before the checkInput method gets called.

The v-for Directive

Just as JavaScript provides for iterating through arrays using loops like the for loop, Vue-js also provides a built-in directive known as the v-for that performs the same function.

We can write the v-for syntax as item in items where items are the array we are iterating over or as items of items to express the similarity with the JavaScript loop syntax.

List Rendering

Let us consider rendering the types of component build steps on a page.

<template>
  <div>
    <ul>
        <li v-for="steps in buildSteps" :key="steps.id"> {{ steps.step }}</li>
      </ul>
  </div>
</template>

<script>
export default {
 data() {
   return {
     buildSteps: [
      {
       id: "step 1",
       step:'With the build step',
      },
      {
        id: "step 2",
       step:'Without the build step'
      }
    ]
   }
 }
}
</script>

In the code block above, the steps array within the data property shows the two types of build steps we have for a component. Within our template, we used the v-for directive to loop through the steps array, the result of which we will render in an unordered list.

We added an optional key argument representing the index of the item we are currently iterating on. But beyond that, the key accepts a unique identifier that enables us to track each item’s node for proper state management.

Using v-for With A Component

Just like using the v-for to render lists, we can also use it to generate components. We can add the v-for directive to the component like the following:

<BuildStep v-for="steps in buildSteps" :key="steps.id"/>

The above code block will not do much for rendering or passing the step to the component. Instead, we will need to pass the value of the step as props to the component.

<BuildStep v-for="steps in buildSteps" :key="steps.id" :buildType="steps.step" />

We do the above to prevent any tight fixation of the v-for to the component.

The most important thing to note in the different usage of the v-for is the automation of a long process. We can move from manually listing out 100 items or components to using the v-for directive and have everything rendered out within the split of a second, as the case may be.

Building A Complex Registration Form In Vue

We will combine everything we have learned about the v-model, Vue components, the Vue props, the v-for directive, and event handling to build a complex form that would help us achieve efficiency, scalability, and time management.

This form will cater to capturing students’ bio-data, which we will develop to facilitate progressive enhancement as business demands increase.

Setting Up The Vue App

We will be scaffolding our Vue application using the build step. To do this, we will need to ensure we have the following installed:

Now we will proceed to create our Vue application by running the command below:

# npm
npm init vue@latest vue-complex-form

where vue-complex-form is the name of the Vue application.

After that, we will run the command below at the root of our Vue project:

npm install

Creating The JSON File To Host The Form Data

We aim to create a form where users can fill in their details. While we can manually add all the input fields, we will use a different approach to simplify our codebase. We will achieve this by creating a JSON file called util/bio-data.json. Within each of the JSON objects, we will have the basic info we want each input field to have.

[
  {
    "id": 1,
    "inputvalue":"  ",
    "formdata": "First Name",
    "type": "text",
    "inputdata": "firstname"
  },
  {
    "id": 2,
    "inputvalue":"  ",
    "formdata": "Last Name",
    "type": "text",
    "inputdata": "lastname"
  },
]

As seen in the code block above, we created an object with some keys already carrying values:

  • id acts as the primary identifier of the individual object;
  • inputvalue will cater to the value passed into the v-model;
  • formdata will handle the input placeholder and the labels name;
  • type denotes the input type, such as email, number, or text;
  • inputdata represents the input id and name.

These keys’ values will be passed in later to our component as props. We can access the complete JSON data here.

Creating The Reusable Component

We will create an input component that will get passed the props from the JSON file we created. This input component will get iterated on using a v-for directive to create numerous instances of the input field at a stretch without having to write it all out manually. To do this, we will create a components/TheInputTemplate.vue file and add the code below:

<template>
  <div>
    <label :for="inputData">{{ formData }}</label>
    <input
      :value= "modelValue"
      :type= "type"
      :id= "inputData"
      :name= "inputData"
      :placeholder= "formData"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  </div>
 </template>
 
<script>
export default {
  name: 'TheInputTemplate',
  props: {
    modelValue: {
      type: String
    },
    formData: {
      type: String
    },
    type: {
      type: String
    },
    inputData: {
      type: String
    }
  },
  emits: ['update:modelValue']
}
</script>
<style>
label {
  display: inline-block;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
  color: rgb(61, 59, 59);
  font-weight: 700;
  font-size: 0.8rem;
}
input {
  display: block;
  width: 90%;
  padding: 0.5rem;
  margin: 0 auto 1.5rem auto;
}
</style>

In the above code block, we achieved the following:

  • We created a component with an input field.
  • Within the input field, we matched the values that we will pass in from the JSON file to the respective places of interest in the element.
  • We also created props of modelValue, formData, type, and inputData that will be registered on the component when exported. These props will be responsible for taking in data from the parent file and passing it down to the TheInputTemplate.vue component.
  • Bound the modelValue prop to the value of the input value.
  • Added the update:modelValue, which gets emitted when the input event is triggered.

Registering The Input Component

We will navigate to our App.vue file and import the TheInputTemplate.vue component from where we can proceed to use it.

<template>
  <form class="wrapper">
    <TheInputTemplate/>
  </form>
</template>
<script>
import TheInputTemplate from './components/TheInputTemplate.vue'
export default {
  name: 'App',
  components: {
    TheInputTemplate
  }
}
</script>
<style>
html, body{
  background-color: grey;
  height: 100%;
  min-height: 100vh;
}
.wrapper {
  background-color: white;
  width: 50%;
  border-radius: 3px;
  padding: 2rem  1.5rem;
  margin: 2rem auto;
}
</style>

Here we imported the TheInputTemplate.vue component into the App.vue file, registered it in the components option property, and then declared it within our HTML template.

If we run npm run serve, we should have the following view:

Application interface just after rendering the input component and registering the component in the App.vue file
Input component after registration. (Large preview)

At this point, there is not much to see because we are yet to register the props on the component.

Passing Input Data

To get the result we are after, we will need to pass the input data and add the props to the component. To do this, we will update our App.vue file:

<template>
  <div class="wrapper">
    <div v-for="bioinfo in biodata" :key="bioinfo.id">
      <TheInputTemplate v-model="bioinfo.inputvalue":formData= "bioinfo.formdata":type= "bioinfo.type":inputData= "bioinfo.inputdata"/>
    </div>
  </div>
<script>
//add imports here
import biodata from "../util/bio-data.json";
export default {
  name: 'App',
 //component goes here
  data: () => ({
    biodata
  })
}
</script>

From the code block above, we achieved several things:

  • We imported the bio-data JSON file we created into the App.vue file. Then we added the imported variable to the data options of the Vue script.
  • Looped through the JSON data, which we instantiated in the data options using the Vue v-for directive.
  • Within the TheInputTemplate.vue component we created, we passed in the suitable data to fill the props option.

At this point, our interface should look like the following:

Interface showing the rendered form after passing the props to the input component
Application view showing the rendered complex form. (Large preview)

To confirm if our application is working as it should, we will open up our Vue DevTools, or install one from https://devtools.vuejs.org if we do not have it in our browser yet.

When we type in a value in any of the input fields, we can see the value show up in the modelValue within the Vue Devtools dashboard.

Vue Devtools view showing the modelValue of the input value
Vue DevTools showing the input value. (Large preview)

Conclusion

In this article, we explored some core Vue fundamentals like the v-for, v-model, and so on, which we later sewed together to build a complex form. The main goal of this article is to simplify the process of building complex forms while maintaining readability and reusability and reducing development time.

If, in any case, there will be a need to extend the form, all the developer would have to do is populate the JSON files with the needed information, and voila, the form is ready. Also, new Engineers can avoid swimming in lengthy lines of code to get an idea of what is going on in the codebase.

Note: To explore more about handling events within components to deal with as much complexity as possible, you can check out this article on using components with v-model.

Further Reading on Smashing Magazine

Smashing Editorial (yk, il)