Editor’s note: This guide to forms in Vue.js was updated on 19 January 2021.
Learning to work with forms properly in our favorite frameworks is valuable, and it can save us some time and energy during development. In this tutorial, I will walk you through the process of creating, validating, and utilizing inputs from a form in a Vue.js v2.x application.
To follow along with this tutorial, you will need some knowledge of HTML and Vue.js. You can play around with the entire demo on CodePen.
See the Pen
Vue Form Playground by Olayinka Omole (@olayinkaos)
on CodePen.
We will start by creating a simple Vue.js app with some basic HTML markup. We will also import Bulma to take advantage of some pre-made styles:
<!DOCTYPE html> <html> <head> <title>Fun with Forms in Vue.js</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.4/css/bulma.min.css"> </head> <body> <div class="columns" id="app"> <div class="column is-two-thirds"> <section class="section"> <h1 class="title">Fun with Forms in Vue 2.0</h1> <p class="subtitle"> Learn how to work with forms, including <strong>validation</strong>! </p> <hr> <!-- form starts here --> <section class="form"> </section> </section> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script> <script> new Vue({ el: '#app' }) </script> </body> </html>
v-model
We can bind form input and textarea
element values to the Vue instance data using the v-model
directive. According to the Vue docs, the v-model
directive enables you to create two-way data bindings on form input, textarea
, and select elements. It automatically picks the correct way to update the element based on the input type.
Let’s get started by creating a simple text input to get a user’s full name:
...
<section class="form">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input v-model="form.name" class="input" type="text" placeholder="Text input">
</div>
</div>
</section>
...
<script>
new Vue({
el: '#app',
data: {
form : {
name: ''
}
}
})
</script>
In the above code, we define the data option in our Vue instance and define a form object, which will hold all the properties we want for our form. The first property we define is name
, which is bound to the text input we also created.
Now that two-way binding exists, we can use the value of form.name
anywhere in our application, as it will be the updated value of the text input. We can add a section to view all the properties of our form object:
...
<div class="columns" id="app">
<!-- // ... -->
<div class="column">
<section class="section" id="results">
<div class="box">
<ul>
<!-- loop through all the `form` properties and show their values -->
<li v-for="(item, k) in form">
<strong>{{ k }}:</strong> {{ item }}
</li>
</ul>
</div>
</section>
</div>
</div>
...
If you’re following along properly, you should have the same result as the fiddle below. Try typing in the input box:
Note that v-model
will ignore the value, checked, or selected attributes of form inputs and will treat the Vue instance data as the source of truth. This means that you can also specify a default value for form.name
in the Vue instance. That is what the initial value of the form input will be.
Textarea
exampleThese work the same way regular input boxes work:
...
<div class="field">
<label class="label">Message</label>
<div class="control">
<textarea class="textarea" placeholder="Message" v-model="form.message"></textarea>
</div>
</div>
...
And the corresponding value in the form model:
data: {
form : {
name: '',
message: '' // textarea value
}
}
It’s important to note that interpolation in textarea
— <textarea>{{ form.message}}</textarea>
— will not work for two-way binding. Use the v-model
directive instead.
v-model
directive for select boxesThe v-model
directive can also be easily plugged in for select boxes. The defined model will be synced with the value of the selected option:
...
<div class="field">
<label class="label">Inquiry Type</label>
<div class="control">
<div class="select">
<select v-model="form.inquiry_type">
<option disabled value="">Nothing selected</option>
<option v-for="option in options.inquiry" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
</div>
...
In the above code, we chose to load the options dynamically using the v-for
directive. This requires us to also define the available options in the Vue instance:
data: {
form : {
name: '',
message: '',
inquiry_type: '' // single select box value
},
options: {
inquiry: [
{ value: 'feature', text: "Feature Request"},
{ value: 'bug', text: "Bug Report"},
{ value: 'support', text: "Support"}
]
}
}
The process is similar for a multi-select box. The difference is that the selected values for the multi-select box are stored in an array.
For example:
...
<div class="field">
<label class="label">LogRocket Usecases</label>
<div class="control">
<div class="select is-multiple">
<select multiple v-model="form.logrocket_usecases">
<option>Debugging</option>
<option>Fixing Errors</option>
<option>User support</option>
</select>
</div>
</div>
</div>
...
And defining the corresponding model property:
data: {
form : {
name: '',
message: '',
inquiry_type: '',
logrocket_usecases: [], // multi select box values
},
// ..
}
In the example above, the selected values will be added to the logrocket_usecases
array.
Single checkboxes which require a boolean (true/false) value can be implemented like this:
...
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="form.terms">
I agree to the <a href="#">terms and conditions</a>
</label>
</div>
</div>
...
Defining the corresponding model property:
>data: {
form : {
name: '',
message: '',
inquiry_type: '',
logrocket_usecases: [],
terms: false, // single checkbox value
},
// ..
}
The value of form.terms
in the example above will either be true or false depending on whether the checkbox is checked or not. A default value of false is given to the property, hence the initial state of the checkbox will be unchecked.
For multiple checkboxes, they can simply be bound to the same array:
...
<div class="field">
<label>
<strong>What dev concepts are you interested in?</strong>
</label>
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="form.concepts"
value="promises">
Promises
</label>
<label class="checkbox">
<input type="checkbox" v-model="form.concepts"
value="testing">
Testing
</label>
</div>
</div>
...
data: {
form : {
name: '',
message: '',
inquiry_type: '',
logrocket_usecases: [],
terms: false,
concepts: [], // multiple checkbox values
},
// ..
}
For radio buttons, the model
property takes the value of the selected radio
button.
Here’s an example:
...
<div class="field">
<label><strong>Is JavaScript awesome?</strong></label>
<div class="control">
<label class="radio">
<input v-model="form.js_awesome" type="radio" value="Yes">
Yes
</label>
<label class="radio">
<input v-model="form.js_awesome" type="radio" value="Yeap!">
Yeap!
</label>
</div>
</div>
...
data: {
form : {
name: '',
message: '',
inquiry_type: '',
logrocket_usecases: [],
terms: false,
concepts: [],
js_awesome: '' // radio input value
},
// ..
}
While writing custom validation logic is possible, there is already a great plugin that helps validate inputs easily and displays the corresponding errors. This plugin is vee-validate.
This form validation library for Vue.js allows you to validate inputs and build form UIs in a declarative style, or using composition functions.
First, we need to include the plugin in our app. This can be done with yarn or npm, but in our case, including it via CDN is just fine:
<script src="https://unpkg.com/[email protected]/dist/vee-validate.js"></script>
To set up the plugin, we will place this just right above our Vue instance:
Vue.use(VeeValidate);
Now, we can use the v-validate
directive on our inputs and pass in the rules as string values. Each rule can be separated by a pipe.
Taking a simple example, let’s validate the name
input we defined earlier:
...
<div class="field">
<label class="label">Name</label>
<div class="control">
<input name="name"
v-model="form.name"
v-validate="'required|min:3'"
class="input" type="text" placeholder="Full name">
</div>
</div>
...
In the above example, we defined two rules: the first is that the name field is required (required
), the second is that the minimum length of any value typed in the name field should be three (min:3
).
Tip: Rules can be defined as objects for more flexibility. For example:
v-validate=”{required: true, min: 3}”
To access the errors when these rules aren’t passed, we can use the errors helper object created by the plugin. For example, to display the errors just below the input, we can add this:
<p class="help is-danger" v-show="errors.has('name')">
{{ errors.first('name') }}
</p>
In the code above, we take advantage of a couple of helper methods from the vee-validate
plugin to display the errors:
.has()
helps us check if there are any errors associated with the input field passed in as a parameter. It returns a boolean value (true/false)..first()
returns the first error message associated with the field passed in as a parameter.Other helpful methods include .collect()
, .all()
, and .any()
. You can read more on them here.
Note that the name attribute needs to be defined on our input fields as this is what vee-validate
uses to identify fields.
Finally, we can add an is-danger
class (provided by Bulma) to the input field to indicate when there’s a validation error for the field. This would be great for the user experience, as users would immediately see when an input field hasn’t been filled properly.
The entire field markup will now look like this:
...
<div class="field">
<label class="label">Name</label>
<div class="control">
<input name="name"
v-model="form.name"
v-validate="'required|min:3'"
v-bind:class="{'is-danger': errors.has('name')}"
class="input" type="text" placeholder="Full name">
</div>
<p class="help is-danger" v-show="errors.has('name')">
{{ errors.first('name') }}
</p>
</div>
...
You can see the work in progress so far in the results tab in the fiddle below:
No Title
No Description
vee-validate
has a good number of predefined rules that cater to the generic use cases . The full list of available rules can be found here. Custom validation rules can also be defined if your form has any needs that aren’t covered by the generic rules.
Validator.extend()
We can create custom rules using the Validator.extend()
method. Your custom rules must adhere to a contract or a certain structure.
Let’s add a validation method that forces our users to be polite when sending messages:
VeeValidate.Validator.extend('polite', {
getMessage: field => `You need to be polite in the ${field} field`,
validate: value => value.toLowerCase().indexOf('please') !== -1
});
Our validator consists of two properties:
getMessage(field, params)
: Returns a string — the error message when validation fails.validate(value, params)
: Returns a boolean, object, or promise. If a boolean isn’t returned, the valid(boolean)
property needs to be present in the object or promise.vue-validate
was also built with localization in mind. You can view notes on translation and localization in the full vue-validate
custom rules docs.
To handle form submissions, we can make use of Vue’s submit event handler. For method event handlers, we’ll use the v-on
method. We can also plug in the .prevent
modifier to prevent the default action, which in this case would be the page refreshing when the form is submitted:
...
<form v-on:submit.prevent="console.log(form)">
...
<div class="field is-grouped">
<div class="control">
<button class="button is-primary">Submit</button>
</div>
</div>
</form>
...
In the above example, we simply log the entire form model to the console on form submission.
We can add one final touch with vee-validate
to make sure that users aren’t allowed to submit an invalid form. We can achieve this using errors.any()
:
<button
v-bind:disabled="errors.any()"
class="button is-primary">
Submit
</button>
In the above code, we disable the submit button once any errors exist in our form.
Great! Now we have built a form from scratch, added validation, and can use the input values from the form. You can find the entire code we have worked on here on CodePen.
Some other key things to note:
v-model
on form inputs, including .lazy
, .number
, and .trim
. You can read all about them here.v-bind:value
. Check the docs on Value Bindings for more info.In this guide, we have learned how to create a form in a Vue.js app, validate it, and work with its field values. We addressed one way to create a form in Vue.js, but there are other options. This tutorial shows you how to create forms with Vue Formulate.
Do you have any questions or comments about forms in Vue.js, or maybe how they compare to forms in other frameworks? Feel free to drop a comment and let’s get talking!
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
8 Replies to "A complete guide to forms in Vue.js"
Great stuff… wish there was a github repo somewhere.
Thanks so much.
Very helpful, especially how to dynamically bind data in stuff. That way if the option list is large (i.e.countries) we can use external file and stop cluttering our page. Thanks
Now available on Github: https://github.com/vaiden/vue-form-example
Hi, great article that covers many of the pain points related to building forms with Vue. There’s a lot of manual work required to bind and get reactivity from each type of element you need to work with.
I’m a core maintainer of Vue Formulate (https://vueformulate.com/) which released a substantial 2.0 update in March including exhaustive documentation for developers looking to implement it in their own projects. I’d be happy to help you put together any code examples using Vue Formulate if you’d be interested in writing about it. I honestly think it represents a substantial quality of life improvement for developers who have the need to write forms with Vue.
Hi Andrew, thanks for sharing. Congrats on the release, the docs look great. Would you be interested in writing a post on your updates? Let me know — mangelosanto[at]logrocket[dot]com
Matt, most certainly, I’ll reach out via email to coordinate. We’re about to release version 2.3 which comes with some substantial new goodies such as full scoped slots support, grouped (and repeatable) fields, and more — So now would be a great time to do a write-up about it.
Can it be that no one noticed that `data` should always return a function instead of being an object?
2024 Update: Use FormKit 😄