Search code examples
vue.jsvuejs2vuelidate

Vuelidate $each: How can I validate a nested collection?


I am having a really hard time trying to grasp the likely elementary concept(s). I am passing a location in as a prop. It has a json column to store additionalAttributes. It looks something like this:

    "additionalProperties": [
        {
            "integrations": [
                {
                    "exampleVendor": {
                        "locationId": 123,
                        "positionId": 456
                    }
                }
            ]
        }
    ],
    "createdAt": "",
    "updatedAt": "",
    ...

The above is what I've hard-coded into my database (Postgres) to attempt to mock what the data will look like when it comes back.

I am working from the validate collections portion of the vuelidate documentation.

Here is what I am using to attempt to create the validation rule:

validations: {
      location: {
        additionalProperties: {
          $each: {
            integrations: {
              $each: {
                exampleVendor: {
                  locationId: {required},
                  positionId: {required},
                }
              }
            }
          }
        }
      }
  },

In my template, I'm trying to connect the validations like this:

<select id="my-id" 
    name="my-id" 
    class="py-3 px-3 mt-1 block w-full pl-3 pr-10 py-2 text-base sm:text-sm rounded-md" 
    v-if="locations"
    v-model.trim="$v.location.additionalProperties[0].integrations[0].exampleVendor.locationId.$model" 
    :class="[$v.location.additionalProperties[0].integrations[0].exampleVendor.locationId.$error ? 
    'focus:ring-red-500 focus:border-red-500 border-red-300' : 'focus:ring-gray-400 focus:border-gray-400 border-gray-300',]"
>
...
</select>

I've been working with this component for quite a while and have already asked a really silly question.

I am also concerned that by setting such a rigid path additionalProperties[0].integrations[0] is really bad.

I fear this one isn't too far behind but it's time to ask for some advice. Thank you for any suggestions!

EDIT

@tony19 made an excellent call about why the array if only the first value is being used. Perhaps there is a better way to do what I'm doing; here is a wider view of what the data in my database could look like. It has additional properties now beyond just integrations. For now, I'm only focused on that though.


"additionalProperties": [
        {
            "integrations": [
                {
                    "exampleVendor": {
                        "locationId": 123,
                        "positionId": 456
                    },
                    "anotherVendor": {
                        "foo": "abc",
                        "bar": "def"
                    },
                    "someOtherVendor": {
                        "thing": "value"
                    }
                }
            ],
            "anotherAttribute: {
                "one": "two"
            },
            "possibleAttributes": [...]
        }
    ],

Solution

  • There are quite a few things I've learned while working through this. One of the more important being how to troubleshoot what vuelidate thought it was getting.

    I created an change handler to provide insight to what the $model value was. Here is an example:

    <select @change="onChange"...">...</select>
    
    
    ...
    
    // start with what I know to be true.
    onChange() {
        console.log($v.location.additionalProperties);
    }
    
    

    Using the above object structure, I'd then move into the object until I ended up with this:

    console.log($v.location.additionalProperties.$each[0].integrations.$each[0]. exampleVendor.locationId.$model; // 12345
    

    Now that I had the "path" to the model, I could update my <select> element:

    <select id="my-locationId" name="my-locationId" class="py-3 px-3 mt-1 block w-full pl-3 pr-10 py-2 text-base sm:text-sm rounded-md" 
        v-model.trim="$v.location.additionalProperties.$each[0].integrations .$each[0].exampleVendor.locationId.$model"
        :class="[
                 $v.location.additionalProperties.$each[0].integrations.$each[0].exampleVendor.locationId.$error
                            ? 'focus:ring-red-500 focus:border-red-500 border-red-300'
                            : 'focus:ring-gray-400 focus:border-gray-400 border-gray-300',
                        ]"
        >
        <option selected="selected" value="">Select</option>
        <option
            v-for="location in myLocations"
            :key="location.id"
            :value="location.id"
        >
            {{ location.name }}
        </option>
    </select>
    

    Now that the nested path was collecting/setting the data, I could set up the validation rules:

    ...
    
    data: () => ({...}),
    
    validations: {
      location: {
          additionalProperties: {
            $each: {
              integrations: {
                $each: {
                  exampleVendor: {
                    locationId: { required },
                    positionId: { required },
                  },
                },
              },
            },
         },
      },
    },
    
    ...
    
    methods: {
      async save() {
          this.$v.$touch();
    
          if (this.$v.$invalid) {
            this.errors = true;
          } else {
            try {
              const params = {
                location: this.location, // location is passed in as props
                method: this.location.id ? "PATCH" : "POST",
              };
    
              console.log('params: ', params);  // {...}
    
              // Save to vuex or ??
            } catch (error) {
              console.log('there was an error:', error);
            }
          }
        },
    }
    

    Hope this helps someone else - it wasn't super straight forward & I'm sure there is a more effective way, but this ended up working for me.

    EDIT 2

    Please be sure to follow @tony19's suggested answer as well. The solution provided removes the "rigidity" I was speaking about in my question.