Search code examples
vue.jsvuelidate

Vuelidate validation of computed property


In my Vue component I have 3 numeric fields, like this:

data() {
    return {
        numberAdults: 0,
        numberChildren: 0,
        numberInfants: 0,
    }

and also a computed property:

computed: {
    numberPersons() {
        return this.numberAdults + this.numberChildren + this.numberInfants
    },

I want to validate that at least one person is set (at least one adult or one child or one infant), but I cannot make this to work. This is the validation rule:

numberPersons: {
    required,
    minValue: minValue(1),
},

If I change the numberPersons with one of the three fields, the validation for that field works. I think that somehow Vuelidate doesn't know what numberPersons is, but I am not sure how to change that.


Solution

  • You can create a custom validation method, say, minNumberPersons that simply validates your computed property this.numberPersons:

    const minNumberPersons = (value, vm) => {
        return vm.numberPersons >= 1;
    };
    

    Then, you can apply these rules to the different models:

    validations: {
      numberAdults: { minNumberPersons },
      numberChildren: { minNumberPersons },
      numberInfants: { minNumberPersons }
    }
    

    UX suggestion, totally optional: On a side note, since you are going to be validating 3 fields at the same time, it makes sense to use the $v.<field>.$touch() method to ensure that the dirty state of all 3 fields are set to true when any of them are changed. You can simply do a @input="onInput" binding in your template, and add this to your methods:

    onInput() {
      this.$v.numberAdults.$touch();
      this.$v.numberChildren.$touch();
      this.$v.numberInfants.$touch();
    },
    

    See proof-of-concept here. I have adapted it from the demo JSFiddle used by the Vuelidate repository.

    Vue.use(window.vuelidate.default);
    
    const numberPersons = (value, vm) => {
      return vm.numberPersons >= 1;
    };
    
    new Vue({
      el: "#app",
      data: {
        numberAdults: 0,
        numberChildren: 0,
        numberInfants: 0,
      },
      validations: {
        numberAdults: { numberPersons },
        numberChildren: { numberPersons },
        numberInfants: { numberPersons }
      },
      computed: {
        numberPersons() {
          // Converting each to a number using the unary + operator, in case user inputs empty string
          return (+this.numberAdults) + (+this.numberChildren) + (+this.numberInfants);
        },
      },
      methods: {
        status(validation) {
          return {
            error: validation.$error,
            dirty: validation.$dirty
          }
        },
    
        // Optional: force validation of all number inputs when any one is changed
        onInput() {
          this.$v.numberAdults.$touch();
          this.$v.numberChildren.$touch();
          this.$v.numberInfants.$touch();
        },
      }
    })
    body {
      background: #fff;
    }
    
    input {
      border: 1px solid silver;
      border-radius: 4px;
      background: white;
      padding: 5px 10px;
    }
    
    .dirty {
      border-color: #5A5;
      background: #EFE;
    }
    
    .dirty:focus {
      outline-color: #8E8;
    }
    
    .error {
      border-color: red;
      background: #FDD;
    }
    
    .error:focus {
      outline-color: #F99;
    }
    <!-- Boilerplate adapted from Vuelidate's default demo: https://jsfiddle.net/Frizi/b5v4faqf/ -->
    
    <script src="https://unpkg.com/vuelidate/dist/vuelidate.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
    
    <div id="app">
      <input v-model.number="$v.numberAdults.$model" :class="status($v.numberAdults)" @input="onInput" type="number">
      <input v-model.number="$v.numberChildren.$model" :class="status($v.numberChildren)" @input="onInput" type="number">
      <input v-model.number="$v.numberInfants.$model" :class="status($v.numberInfants)" @input="onInput" type="number">
      <br />
      <br />
      Total number of people: <strong>{{ numberPersons }}</strong>
    </div>