Building Complex Forms In Vue
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.
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.
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, 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.
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:
- Node.js;
- npm or yarn.
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 thev-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
, andinputData
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 theTheInputTemplate.vue
component. - Bound the
modelValue
prop to the value of the input value. - Added the
update:modelValue
, which gets emitted when theinput
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>
<div class="wrapper">
<TheInputTemplate/>
</div>
</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:
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 thedata
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:
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.
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
- “Optimizing A Vue App,” Michelle Barker
- “Three Insights I Gained While Researching Vue.js Accessibility,” Marcus Herrmann
- “Tools And Practices To Speed Up The Vue.js Development Process,” Uma Victor
- “Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System,” Lisi Linhart