Skip to main content

Vue JS Nested and Wildcard Validation

In this section we will go through a in-depth example on how to validate complex forms that contain nested objects and array of objects.

The form that we aim to validate looks like the following.

http://localhost:3000

Account

Profile

Social Platforms:

Addresses

Address 1

Components Structure

To be able to proceed with the implementation, the form needs to be divided into components. The image below shows the different components used for the example. Let's start by creating the Form directory inside of the components directory.

Vue components

store.js

The first step to be done before creating any component, is to create a reactive store that will hold the ErrorBag instance to be used in all the components. That way the errors can be imported directly in the component instead of being passed from parent to child components.

Go ahead and create a store.js file in the Form directory.

store.js

import { ref } from 'vue';
import { ErrorBag } from 'simple-body-validator';

export const errors = ref(new ErrorBag);

caution

You are not bounded to vue ref function for the state management, you can manage state the way it fits you the most.

index.vue

Create the index.vue file in the Form directory. The index.vue will be used to set the initial data, rules, and to pass them to the validator instance. Let's start with the imports.

    import { errors } from './store';
import { make, Password, ruleIn } from 'simple-body-validator';

Next, we will specify the initial data attributes, along wih their respective rules. You can find all the available rules here.

    data() {
return {
validator: make(),
errors,
data: {
email: '',
password: '',
profile: {
firstName: '',
lastName: '',
gender: '',
socialPlatforms: [],
addresses: [
{
street: '',
city: '',
zipCode: '',
}
]
}
},
rules: {
email: 'required|email',
password: [ 'required', Password.default() ],
profile: {
firstName: 'bail|required|string|min:3|max:30',
lastName: 'bail|required|string|min:3|max:30',
// Gender must match one of the predefined values
gender: [
'required', ruleIn(['Female', 'Male', 'Other'])
],
// The social platform must an array of min 2 items, max 3 items,
// and each item must match one of the predefined values
socialPlatforms: 'bail|array|min:2|max:4',
'socialPlatforms.*': ruleIn(['Facebook', 'Instagram', 'Linkedin', 'Twitter']),
// The address must be an array with at least one item
addresses: 'required|array|min:1',
// Each item in the addresses array must be an object,
'addresses.*': 'object',

// validate the attributes for each address object


'addresses.*.street': 'required|string|min:5|max:255',
// The city is required when zip code doesn't exist
'addresses.*.city': [
'required_without:profile.addresses.*.zipCode',
'string', 'min:5', 'max:255'
],
// The zip Code is required when city doesn't exist
'addresses.*.zipCode': [
'required_without:profile.addresses.*.city', 'digits:5'
],
}
}
}
}

If you take a look at the rules object you will notice that it matches exactly the structure of the data object. Additionally, to validate the array attributes we used the * notation, and in the city validation you might have noticed the following.

    'required_without:profile.addresses.*.zipCode',

In the required_without rule we specified the full path of the attribute, otherwise the library will not be able to map the rule to the correct attribute value. You can find an introduction on Nested and Wildcard rules here.

Now we will use the create() lifecycle method to pass the initial data and rules to the validator instance.

    created() {
this.validator.setData(this.data).setRules(this.rules).setCustomMessages({
socialPlatforms: {
min: 'You must at least select :min platforms'
}
});
},
Set Custom Messages

The setCustomMessages method can be used to override the message generated by the library. To find out more on Customizing Error Messages click here.

import { make, Password, ruleIn, ErrorBag } from 'simple-body-validator';
import { errors } from './store';
import Account from './Account.vue';
import Profile from './Profile.vue';

export default {
components: {
Account,
Profile,
},
data() {
return {
validator: make(),
errors,
data: {
email: '',
password: '',
profile: {
firstName: '',
lastName: '',
gender: '',
socialPlatforms: [],
addresses: [
{
street: '',
city: '',
zipCode: '',
}
]
}
},
rules: {
email: 'required|email',
password: [ 'required', Password.default() ],
profile: {
firstName: 'required|string|min:3|max:30',
lastName: 'required|string|min:3|max:30',
gender: [
'required', ruleIn(['Female', 'Male', 'Other'])
],
socialPlatforms: 'bail|array|min:2|max:4',
'socialPlatforms.*': ruleIn(['Facebook', 'Instagram', 'Linkedin', 'Twitter']),
addresses: 'required|array|min:1',
'addresses.*': 'object',
'addresses.*.street': 'required|string|min:5|max:255',
'addresses.*.city': [
'required_without:profile.addresses.*.zipCode',
'string', 'min:5', 'max:255'
],
'addresses.*.zipCode': [
'required_without:profile.addresses.*.city', 'digits:5'
],
}
}
}
},
created() {
this.validator.setData(this.data).setRules(this.rules).setCustomMessages({
socialPlatforms: {
min: 'You must at least select :min platforms'
}
});
},
methods: {
validateForm(event) {
event.preventDefault();
this.validator.setData(this.data).validate();
this.errors = this.validator.errors();
},
},
}
warning

The index.vue component will not work directly since we didn't create the Account and Profile components yet.

Account.vue

import { errors } from './store';

export default {
props: {
data: {
type: Object,
default: {}
}
},
data() {
return {
errors
}
},
};

Profile.vue

import { errors } from './store';
import SocialPlatform from './SocialPlatform.vue';
import Gender from './Gender.vue';
import AddressesList from './AddressesList.vue';


export default {
components: {
SocialPlatform,
Gender,
AddressesList,
},
props: {
profile: {
type: Object,
default: {},
}
},
data() {
return {
errors
};
},
}
warning

The Profile.vue will not work directly since we didn't create the Gender, SocialPlatform, and AddressesList components yet.

If you take a look at the HTML part of the Profile component, you will notice that when we checked if the error exist we followed the same path specified in the rules object.

<p class="cl-red" v-if="errors.has('profile.firstName')">{{ errors.first('profile.firstName') }}</p>

Gender.vue

import { errors } from './store';
export default {
props: {
selectedGender: String,
},
data() {
return {
errors,
genderList: [
'Female', 'Male', 'Other',
],
};
},
}

SocialPlatform.vue

import { errors } from './store';

export default {
props: {
socialPlatforms: {
type: Array,
default: [],
},
},
data() {
return {
errors,
platforms: [
'Facebook', 'Instagram', 'Twitter', 'Linkedin'
],
};
},
methods: {
handleChange({ target: { checked, value }}) {
if (checked) {
this.socialPlatforms.push(value);
} else {
this.socialPlatforms.splice(this.socialPlatforms.indexOf(value));
}
},
},
}

AddressesList

import { errors } from './store';
import Address from './Address.vue';

export default {
components: {
Address,
},
props: {
addresses: {
type: Array,
default: [],
},
},
data() {
return {
errors
};
},
methods: {
addAddress() {
this.addresses.push({
street: '',
city: '',
zipCode: '',
});
},

removeAddress() {
// Remove the errors related to the address fields
this.errors.forgetAll(`profile.addresses.${this.addresses.length - 1}`);
this.addresses.pop();
},
}
}

In the example above, the index value was passed from the AddressesList component into the Address component. The index will be used to identify the appropriate error messages associated with each address.

Address

import { errors } from './store';

export default {
props: {
index: Number,
address: {
type: Object,
default: {},
}
},
data() {
return {
errors
};
}

};

To be able to get the correct error message for the fields in the address component, we followed the same path of the rules object, and we replaced * with the index of the array.

Initial Rule
    profile: {
'addresses.*.street': 'required|string|min:5|max:255',
}
Error Message
    errors.first(`profile.addresses.${index}.street`);

Full Example Code