Vue.js for beginners, lesson 10: forms

Today, in Lesson 10 of the Vue course, we'll talk about how to work with forms. Forms allow you to collect data entered by the user. In addition, here we will discuss form validation, that is, checking what is entered into them.







β†’ Vue.js beginners lesson 1: instance Vue

β†’ Vue.js for beginners, lesson 2: binding attributes

β†’ Vue.js beginners lesson 3: conditional rendering

β†’ Vue.js beginners lesson 4: lists rendering

β†’ Vue .js for beginners lesson 5: event processing

β†’ Vue.js beginners lesson 6: binding classes and styles

β†’ Vue.js beginners lesson 7: calculated properties

β†’ Vue.js beginners lesson 8: components

β†’ Vue. js for beginners lesson 9: custom events



The purpose of the lesson



We're going to create a form that allows site visitors to submit product reviews. In this case, it is necessary that the review could be sent only if all the fields of the form are filled in, which must be filled in.



Initial code



Here's what's in now index.html:



<div id="app">
  <div class="cart">
    <p>Cart({{ cart.length }})</p>
  </div>

  <product :premium="premium" @add-to-cart="updateCart"></product>
</div>


It looks like this main.js:



Vue.component('product', {
  props: {
    premium: {
      type: Boolean,
      required: true
    }
  },
  template: `
  <div class="product">
    <div class="product-image">
      <img :src="image" />
    </div>

    <div class="product-info">
      <h1>{{ title }}</h1>
      <p v-if="inStock">In stock</p>
      <p v-else>Out of Stock</p>
      <p>Shipping: {{ shipping }}</p>

      <ul>
        <li v-for="detail in details">{{ detail }}</li>
      </ul>
      <div
        class="color-box"
        v-for="(variant, index) in variants"
        :key="variant.variantId"
        :style="{ backgroundColor: variant.variantColor }"
        @mouseover="updateProduct(index)"
      ></div>

      <button
        v-on:click="addToCart"
        :disabled="!inStock"
        :class="{ disabledButton: !inStock }"
      >
        Add to cart
      </button>

    </div>
  </div>
  `,
  data() {
    return {
      product: 'Socks',
      brand: 'Vue Mastery',
      selectedVariant: 0,
      details: ['80% cotton', '20% polyester', 'Gender-neutral'],
      variants: [
        {
          variantId: 2234,
          variantColor: 'green',
          variantImage: './assets/vmSocks-green.jpg',
          variantQuantity: 10
        },
        {
          variantId: 2235,
          variantColor: 'blue',
          variantImage: './assets/vmSocks-blue.jpg',
          variantQuantity: 0
        }
      ]
    }
  },
    methods: {
      addToCart() {
        this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);
      },
      updateProduct(index) {
        this.selectedVariant = index;
        console.log(index);
      }
    },
    computed: {
      title() {
        return this.brand + ' ' + this.product;
      },
      image() {
        return this.variants[this.selectedVariant].variantImage;
      },
      inStock() {
        return this.variants[this.selectedVariant].variantQuantity;
      },
      shipping() {
        if (this.premium) {
          return "Free";
        } else {
          return 2.99
        }
      }
    }
})

var app = new Vue({
  el: '#app',
  data: {
    premium: true,
    cart: []
  },
  methods: {
    updateCart(id) {
      this.cart.push(id);
    }
  }
})


Task



We need site visitors to be able to leave reviews on products, but our site does not yet have the means to receive data from users. Forms are such means.



The solution of the problem



To solve the task before us, we need to create a form. Let's start by creating a new component specifically for working with a form. Let's call this component product-review. This name was chosen because the component will support the form for collecting product reviews. The component product-reviewwill be nested within the component product.



Let's register a new component, start forming its template and equip it with some data:



Vue.component('product-review', {
  template: `
    <input>
  `,
  data() {
    return {
      name: null
    }
  }
})


As you can see, there is an element in the component template <input>, and there is a property in the component data data, while it is empty.



How do I bind what the user enters into a field to a property name?



In the previous lessons, we talked about data binding using directives v-bind, but then we only considered one-way binding. The data flow went from the property that stores the data to the control that renders it. And now we need that what the user enters into the field would be in the property namestored in the component data. In other words, we want the data flow to be directed from the input field to the property.



V-model directive



The directive v-modelallows organizing two-way data binding. With this scheme of work, if something new appears in the input field, this leads to a change in the data. And, accordingly, when the data changes, the state of the control that uses this data is updated.



Let's add a directive to the input field v-modeland bind this field to a property namefrom the component data.



<input v-model="name">


Now let's add the complete form code to the component template:



<form class="review-form" @submit.prevent="onSubmit">
  <p>
    <label for="name">Name:</label>
    <input id="name" v-model="name" placeholder="name">
  </p>

  <p>
    <label for="review">Review:</label>
    <textarea id="review" v-model="review"></textarea>
  </p>

  <p>
    <label for="rating">Rating:</label>
    <select id="rating" v-model.number="rating">
      <option>5</option>
      <option>4</option>
      <option>3</option>
      <option>2</option>
      <option>1</option>
    </select>
  </p>

  <p>
    <input type="submit" value="Submit">  
  </p>

</form>


As you can see, the v-modelfields input, textareaand are equipped with the directive select. Please note that when setting up the field select, a modifier was used .number(we will talk about it in more detail below). This allows you to convert the corresponding data to a type Number, while it is usually represented in a string.



Let's supplement the data set of the component by adding to it the data to which the above-described controls are bound:



data() {
  return {
    name: null,
    review: null,
    rating: null
  }
}


At the top of the form template, you can see that when the form is submitted, a method is called onSubmit. We'll be creating this method soon. But first, let's talk about the role of construction .prevent.



This is an event modifier. It prevents the page from being reloaded after the event is raised submit. There are other useful event modifiers as well . True, we will not talk about them.



We are now ready to create a method onSubmit. Let's start with this code:



onSubmit() {
  let productReview = {
    name: this.name,
    review: this.review,
    rating: this.rating
  }
  this.name = null
  this.review = null
  this.rating = null
}


As you can see, this method creates an object based on the data entered by the user. A reference to it is written to a variable productReview. Here we drop in nullproperty values name, review, rating. But the work is not over yet. We still need to send somewhere productReview. Where to send this object?



It makes sense to store product reviews in the same place where component data is stored product. Given that the component is product-reviewnested within a component product, we can say that it product-reviewis a child of the component product. As we learned in the previous lesson, you can use events generated by using to send data from child components to parent components $emit.



Let's refine the method onSubmit:



onSubmit() {
  let productReview = {
    name: this.name,
    review: this.review,
    rating: this.rating
  }
  this.$emit('review-submitted', productReview)
  this.name = null
  this.review = null
  this.rating = null
}


Now we generate an event with a name review-submittedand pass the newly created object in it productReview.



Next, we need to organize listening for this event by placing the productfollowing in the template :



<product-review @review-submitted="addReview"></product-review>


This line reads like this: "When an event occurs review-submitted, the addReviewcomponent method needs to be run product."



Here is the code for this method:



addReview(productReview) {
  this.reviews.push(productReview)
}


This method takes the object productReviewthat came from the method onSubmit, and then puts it into an array reviewsstored in the component data product. But there is no such array in the data of this component yet. So let's add it there:



reviews: []


Wonderful! The form elements are now bound to the component data product-review. This data is used to create the object productReview. And this object is passed, when the form is submitted, to the component product. As a result, the object is productReviewadded to the array reviews, which is stored in the component data product.



Displaying product reviews



Now all that remains is to display on the product page the reviews left by site visitors. We will do this in the component productby placing the corresponding code above the code with which the component product-reviewis placed in the component product.



<div>
 <h2>Reviews</h2>
 <p v-if="!reviews.length">There are no reviews yet.</p>
 <ul>
   <li v-for="review in reviews">
   <p>{{ review.name }}</p>
   <p>Rating: {{ review.rating }}</p>
   <p>{{ review.review }}</p>
   </li>
 </ul>
</div>


Here we create a list of reviews using the directive v-forand display the data stored in the object reviewusing dot notation. 



In the tag, <p>we check if there is something in the array reviews(by checking its length). If there is nothing in the array, we print a message There are no reviews yet.





Feedback form page



Form validation



Forms often contain fields that must be filled in before submitting the form. For example, we don't want users to submit reviews in which the review text field is empty.



Luckily for us, HTML5 supports the required. Its use looks like this:



<input required >


Such a construction will lead to an automatic display of an error message if the user tries to submit a form in which the required field is empty.





Error message



Having standard form field validators in the browser is very nice, as it can free us from creating our own field validators. But the way the standard data check is carried out may, in some special case, not suit us. In this situation, it makes sense to write your own form validation code.



Custom form validation



Let's talk about how to create your own form validation system.



Let's include an product-reviewarray for storing error messages into the component data . Let's call it errors:



data() {
  return {
    name: null,
    review: null,
    rating: null,
    errors: []
  }
}


We would like to add to this array information about errors that occur in situations where form fields are empty. We are talking about the following code:



if(!this.name) this.errors.push("Name required.")
if(!this.review) this.errors.push("Review required.")
if(!this.rating) this.errors.push("Rating required.")


The first of these lines tells the system nameto place errorsan error message in the array if the field is empty Name required. Other strings that validate the reviewand fields work similarly rating. If any of them is empty, arrayan error message will be sent to the array .



Where to put this code?



Since we want the error messages to be written to the array only when, when trying to submit the form, it turns out that the fields are name, reviewor are submitnot filled, we can place this code in the method onSubmit. In addition, we will rewrite the code that is already in it, adding some checks to it:



onSubmit() {
  if(this.name && this.review && this.rating) {
    let productReview = {
      name: this.name,
      review: this.review,
      rating: this.rating
    }
    this.$emit('review-submitted', productReview)
    this.name = null
    this.review = null
    this.rating = null
  } else {
    if(!this.name) this.errors.push("Name required.")
    if(!this.review) this.errors.push("Review required.")
    if(!this.rating) this.errors.push("Rating required.")
  }
}


Now we check the fields name, reviewand rating. If there is data in all these fields, we create an object based on them productReviewand send it to the parent component. Then the corresponding properties are reset.



If at least one of the fields turns out to be empty, we put errorsan error message in the array , depending on what the user did not enter before submitting the form.



All that remains is to display these errors, which can be done with the following code:



<p v-if="errors.length">
  <b>Please correct the following error(s):</b>
  <ul>
    <li v-for="error in errors">{{ error }}</li>
  </ul>
</p>


Here a directive is used v-ifwith which we check the array errorsfor the presence of error messages in it (in fact, we analyze the length of the array). If there is something in the array, an element is displayed <p>, which, when applied v-for, displays a list of errors from the array errors.





Error Messages



We now have our own form validation system.



Using the .number modifier



The modifier .numberused in the directive v-modelcan be very useful. But when applying it, keep in mind that there is one problem associated with it. If the corresponding value is empty, it will be represented as a string, not a number. The Vue Recipe Book offers a solution to this problem. It consists in explicitly converting the corresponding value to a numeric type:



Number(this.myNumber)


Workshop



Add the following question to the form: "Would you recommend this product?" Make it so that the user can answer it using the "yes" and "no" radio buttons. Check the answer to this question and include the relevant data in the object productReview.





Outcome



Today we talked about working with forms. Here's the most important thing you've learned today:



  • You can use the directive to organize two-way data binding to form elements v-model.
  • The modifier .numbertells Vue to cast the corresponding value to a numeric type. But when working with it, there is one problem related to the fact that empty values ​​remain strings.
  • The event modifier .preventallows you to prevent page reloading after the form is submitted.
  • With Vue, you can implement a fairly simple mechanism for custom form validation.


Did you do your homework today?



β†’ Vue.js beginners lesson 1: instance Vue

β†’ Vue.js for beginners, lesson 2: binding attributes

β†’ Vue.js beginners lesson 3: conditional rendering

β†’ Vue.js beginners lesson 4: lists rendering

β†’ Vue .js for beginners lesson 5: event processing

β†’ Vue.js beginners lesson 6: binding classes and styles

β†’ Vue.js beginners lesson 7: calculated properties

β†’ Vue.js beginners lesson 8: components

β†’ Vue. js for beginners lesson 9: custom events






All Articles