One of the most important activities in web applications is form interaction. We use forms for user authentication, providing feedback in the form of comments and answering online questionnaires, rating things such as books or contributing to polls, filling in address information on e-commerce websites, writing e-mails, and the list goes on.
Such user interaction in web apps which to a large extent involves forms proves their superiority since they possess features beyond presentational capabilities where data flows in one direction only. Businesses would get responsive feedback from their customers or even go as far as communicating with their customers when using chat tools embedded on customer-care web pages. The same couldn't be said of media such as magazines or TV ads.
The v-model directive
Vue.js enables us to manipulate form data via the special directive v-model. The v-model is different from the v-bind directive since it supports two-way data binding, meaning, data can be updated in both the presentational part, in this case, the template form fields, and the controller part of the app instance.
Let's see an example using the v-model directive.
<html>
<div id="app">
<h2>Login Form</h2>
<form>
<input type="text" v-model="msg">
<br>
{{ msg }}
</form>
</div>
<script src="/js/vue.global.js"></script>
<script>
let app = Vue.createApp({
data() {
return {
msg: "Hello, World!"
}
}
}).mount("#app");
</script>
</html>
Try to edit the message inside the input field in the above example to see it getting updated on the mustache output.
Trying to update the msg field inside our Vue.js instance above will also update the form field. Add the following code to the JavaScript above to see the changes.
mounted(){
let addRandomText = () => {
this.msg = this.msg + " other text!"
}
setTimeout(addRandomText, 3000);
}
So, not only can data be updated from the two parts of our app, the variable also stays in sync in both parts, i.e. changes to the variable from the template are reflected by it inside the Vue.js instance and vice-versa.
Binding form text input
Binding form text is as straightforward as demonstrated in the above example, you need to initiate the variable inside the data property and bind it to the v-model directive on the input element.
Let's see another example using the textarea form field.
<textarea v-model="longMsg"></textarea>
<br>
{{ longMsg }}
return {
msg: "Hello, World!",
longMsg: "This is a very loooooooooooooooooooooooooooooooooooooooooooooooooooooooong paragraph."
}
Add the above code to the respective template and JavaScript parts of the previous example.
Binding form number input
To bind numbers in form input fields you need to set the type to "number" and append .number to the v-model directive, making sure you initiate the number variable inside the data property with a variable of the Number type. [explain a bit about the .number suffix]
Let's see a number input example.
<input type="number" v-model="someNumber">
<br>
{{ someNumber }}
{{ longMsg }}
return {
someNumber: 0,
}
Binding form select fields
With select form fields, add the v-model directive with the data property variable in the select field. When an option is selected from the select field, the String or Number set on the value attribute of the option element is assigned to the variable, otherwise, the label is selected.
<select id="age-select" v-model="selectedAge">
<option v-for="item in options" :value="item.key">{{ item.label }}</option>
</select>
<br>
{{ selectedAge }}
return {
options: [{key: 12, label: '12yrs'}, {key: 14, label: '14 yrs'}],
selectedAge: 0
}
Binding form input radio fields
With form radio inputs, you set the values of the dial input fields to the exact variable you want the bound data property to be set to when a radio choice is selected. You should bind the same data property to all of the radio fields belonging to the same query.
<label for="dials">Are you a Vue Noob?</label>
<label>
<input type="radio" id="dials" v-model="isANoob" value="Yes I am"> Yes
</label>
<label>
<input type="radio" id="dials" v-model="isANoob" value="Not I'm not"> No
</label>
<br>
{{ isANoob }}
return {
isANoob: "Yes I am",
}
It's good practice to initiate radio field data variables with a default choice so as not to receive undesired data submissions.
Binding form checkboxes
Since checkbox checks resolve to a true when checked and false when unchecked, unlike the dial input, all check options should be assigned to separate variables.
Here's a checkbox example.
<label for="chk-one">What Web technologies are you familiar with?</label>
<label>
<input type="checkbox" v-model="chkOne"> HTML
</label>
<label>
<input type="checkbox" v-model="chkTwo"> JavaScript
</label>
<label>
<input type="checkbox" v-model="chkThree"> CSS
</label>
<br>
HTML: {{ chkOne }} JavaScript: {{ chkTwo }} CSS: {{ chkThree }}
return {
chkOne: true,
chkTwo: false,
chkThree: false
}
While dealing with HTML forms, when users have completed filling in the required information we usually expect some action to be performed to proceed with the next desired steps. Normally when HTML forms are submitted via an input field with the type="submit" or a button click, the page sends the form data to the URL provided in the action attribute using the HTTP method provided in the method form attribute followed by a page reload or redirection to a URL, all depending on the response it receives from the back-end in question.
In Vue.js, just as with most front-end JavaScript environments, we do not favour the page refreshes as those would certainly mess up our app states.
Let's demonstrate this in an example.
<html>
<div id="app">
<form>
<input type="text" v-model="name">
<br>
<button type="submit">Submit</button>
</form>
</div>
<script src="/js/vue.global.js"></script>
<script>
let app = Vue.createApp({
data() {
return {
name: "Noob",
}
}
}).mount("#app");
</script>
</html>
Clicking "Submit" in the above example refreshes the page, and we lose the number that we had before after the title - "Cool Form".
In JavaScript front-end frameworks we are usually submitting form data to remote back-ends through API endpoints, then carry on with the next steps depending on the responses we receive without refreshing the page, keeping the rest of the app state intact.
Event handling in Vue
To carry out form submissions, we need to listen to button click events and call some functions that process and submit this data to where it needs to go.
In Vue.js, event listening on HTML elements is performed through the v-on directive. Just as in the case with the v-bind directive, we add the v-on directive as a prefix to element events separated by a colon, removing the "on" usually found on elements' event attributes, meaning, when a button onclick
attribute fires in response to a button click event, we perform a method call - onclick="function()"
. In Vue.js templates, we would have the regular event attributes only replacing the on
in the beginning with v-on:
.
Let's see an example.
<html>
<div id="app">
<h2>Count: {{ num }}</h2>
<button v-on:click="num++">Increment</button>
</div>
<script src="/js/vue.global.js"></script>
<script>
let app = Vue.createApp({
data() {
return {
num: 0,
}
}
}).mount("#app");
</script>
</html>
In the above example, we increment the value of num by one each time the button is clicked, we do that by resolving the JavaScript expression num++
inside v-on:click
.
Let's see another example.
<input type="text" v-model="text" v-on:blur="text = text + ' a'">
data() {
return {
text: 'Hello!'
}
}
Try clicking in and out of the above input field to see what happens.
In both cases, when the click and blur events are fired, the JavaScript expressions are resolved.
Unlike the mustache syntax which is used for presentational purposes, the v-on directive can run JavaScript expressions and update our data properties values as demonstrated in the above examples.
The v-on shorthand
Just like Vue.js provides the colon :
as the shorthand for the v-bind directive, it also provides an "at" sign - @
as the shorthand for the v-on directive. So, instead of using the long v-on
in the above examples, we can shorten the code by using the @
, ending up with.
<button @click="num++">Increment</button>
<input type="text" v-model="text" @blur="text = text + ' a'">
We will be using the shorthand version of the v-on directive here-onwards.
We have already seen how we can execute JavaScript expressions inside the v-on directive, next, we are going to see how we can call functions with it. Let's first see how to initiate functions in Vue.js.
Methods in Vue
Methods or functions in Vue.js are initiated inside the "methods" option of the Vue.js instance. Just as we've seen with the data properties, we add "methods" directly as a property of the Object passed inside createApp() with all our functions as the direct properties of this "method" Object.
Moving the in-line expressions in the previous example into individual methods, we can then proceed to make function calls reducing the logic within our template as follows.
<html>
<div id="app">
<h2>Count: {{ num }}</h2>
<button @click="increment()">Increment</button>
<hr>
<input type="text" v-model="text" @blur="appendText()">
<hr>
num + length of text: {{ numPlusText }}
</div>
<script src="/js/vue.global.js"></script>
<script>
let app = Vue.createApp({
data() {
return {
num: 0,
text: 'Hello!'
}
},
computed: {
numPlusText(){
return this.num + this.text.length;
console.log(this.numPlusText);
}
},
methods: {
increment(val){
this.num++
},
appendText(){
this.text = this.text + ' a';
console.log(this.numPlusText);
}
}
}).mount("#app");
</script>
</html>
In contrast to computed properties, methods do not cache their results, updates can be made to variables within them, and even DOM mutating operations can be performed within them.
N.B., Each time a method is invoked, all of the operations inside it are performed. Due to this, it is a good practice to perform expensive operations that don't involve non-reactive dependencies such as Date.now()
and Math.random()
inside computed properties.
Inside Vue.js methods, we can access all the data and computed properties inside this
as demonstrated above.
Open your browser's console log to see the computed property value numPlusText being logged out whenever the two functions above are called.
Completing form operations
Bringing together all of what we have learned in this post let's set up a login form, and submit its data when a button click attribute event is fired, calling a specific login method that validates our form data before sending it to the external API endpoint.
<html>
<div id="app">
<div v-if="!loggedIn">
<h2>User Login</h2>
<br>
<form action="">
<p v-show="validationError">Error: {{ validationError }}</p>
<label for="email">Email: </label>
<input type="email" id="email" v-model="email" required>
<br>
<br>
<label for="password">Password: </label>
<input type="text" id="password" v-model="password" required>
<br>
<br>
<button @click.prevent="logIn()">{{ loading ? 'loading..' : 'Log In' }}</button>
</form>
</div>
<div v-else>
<h2>Use Dashboard</h2>
<p>Successfully Logged In!</p>
</div>
</div>
<script src="/js/vue.global.js"></script>
<script>
let app = Vue.createApp({
data() {
return {
email: "",
password: "",
loggedIn: false,
validationError: "",
loading: false
}
},
methods: {
logIn(){
this.validationError = "";
let errorsCount = 0;
if(this.password.length < 4){
this.validationError = "Password is not valid";
errorsCount++;
}
const simulateAuthentication = () => {
this.loading = false;
this.loggedIn = true;
}
if(errorsCount === 0){
this.loading = true;
setTimeout(simulateAuthentication, 3500);
}
}
}
}).mount("#app");
</script>
</html>
When you submit the above form after filling in the required fields correctly, you will experience a page refresh and lose the app state that helps our app run correctly. This is the default HTML form behavior as explained above, it is not "a bug" in Vue.js.
Usually, when intending to make JavaScript HTTP requests instead of using the form element default behavior to perform form data submissions, we prevent it by calling preventDefault() of the Event interface inside our control functions. To prevent the default form behavior in the case of Vue.js, we append the suffix .prevent
at the end of our event listener @click
, ending up with @click.prevent="logIn()"
.
Perform this change to the above code to fix the reload issue.
In the example above, we are simulating the login process with a timer function, in real-life applications we use HTTP clients to make these calls and process all of the possible responses that we might receive from the remote back-ends.