Displaying error messages in vuetify when validating nested object with vuelidate

Anima-t3d picture Anima-t3d · Jul 1, 2018 · Viewed 24.3k times · Source

I am using vuelidate to validate my form input and display the error messages using vuetifyjs. I managed to do the basic object validation and am able to show the error messages.

However I'm having issues with displaying the error messages when I validate a collection.

ISSUE

Example data structure:

contact: {
  websites: [
    {
      url: 'http://www.something.com',
      label: 'Website',
    }
  ]
}

Example validation:

validations: {
  websites: {
    $each: {
      url: {
        url,
      }
    }
  },
}

Example template:

<template v-for="(website, index) in websites">
        <v-layout row :key="`website${index}`">
          <v-flex xs12 sm9 class="pr-3">
            <v-text-field
                    label="Website"
                    :value="website.url"
                    @input="$v.websites.$touch()"
                    @blur="$v.websites.$touch()"
                    :error-messages="websiteErrors"
            ></v-text-field>
          </v-flex>
        </v-layout>
</template>

Example computed error message:

websiteErrors() {
        console.log('websites',this.$v.websites) // contains $each
        const errors = []
        if (!this.$v.websites.$dirty) {
          return errors
        }
        // Issue is that all of them show must be valid, even if they are valid. 
        // Validation is basically broken.
        // I also tried this.$v.websites.$each.url
        !this.$v.websites.url && errors.push('Must be valid url')
        return errors
      },

Example method (Update, also tried method with passing index):

websiteErrors(index) {
        console.log('this.$v.entity.websites', this.$v.entity.websites.$each.$iter, this.$v.entity.websites.$each.$iter[index], this.$v.entity.websites.minLength, this.$v.entity.websites.$each.$iter[index].url)
        const errors = []
        if (!this.$v.entity.websites.$dirty) {
          return errors
        }

        !this.$v.entity.websites.$each.$iter[index].url && errors.push('Must be valid url')
        return errors
      },

However when I do this, it will always be true and therefore never show the error.

EXPECTED

I would like to have the same example working as seen in vuelidate sub-collection validation The difference is instead of looping in the template I would like to generate the message programmatically.

REFERENCE

Example provided by vuelidate:

import { required, minLength } from 'vuelidate/lib/validators'

export default {
  data() {
    return {
      people: [
        {
          name: 'John'
        },
        {
          name: ''
        }
      ]
    }
  },
  validations: {
    people: {
      required,
      minLength: minLength(3),
      $each: {
        name: {
          required,
          minLength: minLength(2)
        }
      }
    }
  }
}

<div>
  <div v-for="(v, index) in $v.people.$each.$iter">
    <div class="form-group" :class="{ 'form-group--error': v.$error }">
      <label class="form__label">Name for {{ index }}</label>
      <input class="form__input" v-model.trim="v.name.$model"/>
    </div>
    <div class="error" v-if="!v.name.required">Name is required.</div>
    <div class="error" v-if="!v.name.minLength">Name must have at least {{ v.name.$params.minLength.min }} letters.</div>
  </div>
  <div>
    <button class="button" @click="people.push({name: ''})">Add</button>
    <button class="button" @click="people.pop()">Remove</button>
  </div>
  <div class="form-group" :class="{ 'form-group--error': $v.people.$error }"></div>
  <div class="error" v-if="!$v.people.minLength">List must have at least {{ $v.people.$params.minLength.min }} elements.</div>
  <div class="error" v-else-if="!$v.people.required">List must not be empty.</div>
  <div class="error" v-else-if="$v.people.$error">List is invalid.</div>
  <button class="button" @click="$v.people.$touch">$touch</button>
  <button class="button" @click="$v.people.$reset">$reset</button>
  <tree-view :data="$v.people" :options="{rootObjectKey: '$v.people', maxDepth: 2}"></tree-view>
</div>

Answer

Anima-t3d picture Anima-t3d · Jul 16, 2018

WHAT WENT WRONG

  1. Shared computed property which causes the issue where all siblings share the same error message. (Solved by writing it inline)
  2. Reactivity not triggered due to the array not being updated in a "reactive way" (Make sure to take note of Change Detection Caveats in this case instead of updating the index: I copy the array, replace item and then set the whole array.)
  3. Wrong place to use vuelidate $each.$iter: Moved it from computed error message to v-for

SOLUTION

This is how to do it (Fixes 1 & 3):

<template v-for="(v, index) in $v.websites.$each.$iter">
  <v-layout row :key="`website${index}`">
    <v-flex xs12 sm9 class="pr-3">
      <v-text-field
                    label="Website"
                    :value="v.$model.url"
                    @input="$v.websites.$touch()"
                    @blur="$v.websites.$touch()"
                    :error-messages="v.$dirty && !v.required ? ['This field is required'] : !v.url ? ['Must be a valid url'] : []"
      />
    </v-flex>
  </v-layout>
</template>

This is how my update method is now (Fixes 2):

  updateWebsite(index, $event) {
    const websites = [...this.websites];
    websites[index] = $event;
    this.updateVuex(`websites`, websites)
    this.$v.websites.$touch()
  },

Originally it was like this:

  updateWebsite(index, $event) {
    this.updateVuex(`websites[${index}]`, $event)
    this.$v.websites.$touch()
  },

ALTERNATIVE

There is another option, which is to wrap in this case website inside a component. That way you can keep the computed error message as it will not be shared.