I'm looking for a good and reusable approach to access possible existing nested properties (nested object and array of objects) inside an error object without typeErrors.
I have a createCompany form/page with the following data
data() {
return {
company: {
same_billing_address: true,
physical_address: {},
billing_address: {},
contacts: [
{
function: '',
first_name: '',
last_name: '',
phone: '',
gender: 'female',
email: '',
language: 'nl',
date_of_birth: '',
},
],
},
validationErrors: {},
}
}
The form itself looks like this
<form @submit.prevent="createCompany" @keydown.enter="$event.preventDefault()" class="divide-y">
<fieldset class="pb-6">
<header>
<h3 class="mb-3 text-lg leading-6 font-medium text-gray-900">{{ $tc('general', 1) }}</h3>
</header>
<div class="grid grid-cols-12 gap-x-6 gap-y-3">
<div class="col-span-12">
<InputWithButton :label="$tc('enterprise_number', 1)" buttonLabel="Get enterprise data" :onClick="getEnterpriseData" type="text" id="enterprise_number" v-model="company.enterprise_number" :error="validationErrors.enterprise_number" />
</div>
<div class="col-span-6">
<Input :label="$tc('business_name', 1)" type="text" id="companyName" v-model="company.business_name" :error="validationErrors.business_name" />
</div>
<div class="col-span-6">
<Input :label="$tc('legal_entity_type', 1)" type="text" id="companyType" v-model="company.legal_entity_type" :error="validationErrors.legal_entity_type" />
</div>
<div class="col-span-3">
<Input :label="$tc('phone', 1)" type="text" id="phone" v-model="company.phone" :error="validationErrors.phone" />
</div>
<div class="col-span-6">
<Input :label="$tc('email_address', 1)" type="text" id="email" v-model="company.email" :error="validationErrors.email" />
</div>
</div>
</fieldset>
<fieldset class="py-6">
<header>
<h3 class="mb-3 text-lg leading-6 font-medium text-gray-900">{{ $tc('physical_address', 1) }}</h3>
</header>
<div class="grid grid-cols-12 gap-x-6 gap-y-3">
<div class="col-span-8">
<Input :label="$tc('street', 1)" type="text" id="street" v-model="company.physical_address.street" :error="validationErrors.physical_address.street" />
</div>
<div class="col-span-2">
<Input :label="$tc('number', 1)" type="text" id="number" v-model="company.physical_address.number" :error="validationErrors.physical_address.number" />
</div>
<div class="col-span-2">
<Input :label="$tc('addition', 1)" optional type="text" id="addition" v-model="company.physical_address.addition" :error="validationErrors.physical_address.addition" />
</div>
<div class="col-span-8">
<SelectWithSearch :label="$tc('city', 1)" id="billing_address_postal_code_id" v-model="company.physical_address.postal_code_id" :options="cityOptions" displayProperty="display_name" valueProperty="id" :minLengthForDropdown="3" :error="validationErrors.physical_address.zip_city" />
</div>
<div class="col-span-4">
<Input :label="$tc('country', 1)" type="text" id="country" v-model="company.physical_address.country" :error="validationErrors.physical_address.country" />
</div>
</div>
</fieldset>
<fieldset class="py-6">
<header>
<h3 class="mb-3 text-lg leading-6 font-medium text-gray-900">{{ $tc('billing_address', 1) }}</h3>
</header>
<div class="grid grid-cols-12 gap-x-6 gap-y-3">
<div class="col-span-12">
<Checkbox :label="$tc('billing_same_as_physical', 1)" v-model="company.same_billing_address" :error="validationErrors.same_billing_address" />
</div>
<template v-if="!company.same_billing_address">
<div class="col-span-8">
<Input :label="$tc('street', 1)" type="text" id="street" v-model="company.billing_address.street" :error="validationErrors.billing_address.street" />
</div>
<div class="col-span-2">
<Input :label="$tc('number', 1)" type="text" id="number" v-model="company.billing_address.number" :error="validationErrors.billing_address.number" />
</div>
<div class="col-span-2">
<Input :label="$tc('addition', 1)" type="text" id="addition" v-model="company.billing_address.addition" :error="validationErrors.billing_address.addition" />
</div>
<div class="col-span-8">
<SelectWithSearch :label="$tc('city', 1)" id="billing_address_postal_code_id" v-model="company.billing_address.postal_code_id" :options="cityOptions" displayProperty="display_name" valueProperty="id" :minLengthForDropdown="3" :error="validationErrors.billing_address.zip_city" />
</div>
<div class="col-span-4">
<Input :label="$tc('country', 1)" type="text" id="country" v-model="company.billing_address.country" :error="validationErrors.billing_address.country" />
</div>
</template>
</div>
</fieldset>
<fieldset class="py-6">
<div class="flex justify-between mb-3">
<header>
<h3 class="text-lg leading-6 font-medium text-gray-900">{{ $tc('contact', company.contacts.length) }}</h3>
</header>
<button type="button" class="text-sm leading-6 font-medium text-blue-500 flex items-center" @click="addContact">{{ $tc('add', 1) }} {{ $tc('contact', 1).toLowerCase() }}</button>
</div>
<section class="space-y-6">
<div v-for="(contact, contactIdx) in company.contacts" :key="contactIdx">
<h4 v-show="company.contacts.length > 1" class="mb-3 text-sm leading-6 font-medium text-gray-500">
{{ $tc('contact', 1) }} {{ contactIdx + 1 }} <span @click="deleteContact(contactIdx)" class="text-blue-500 cursor-pointer select-none">({{ $tc('delete', 1) }})</span>
</h4>
<div class="grid grid-cols-12 gap-x-6 gap-y-3">
<div class="col-span-12">
<RadioButtonGroup :label="$tc('gender', 1)" :options="genderOptions" v-model="contact.gender" :error="contacts[contactIdx].gender" />
</div>
<div class="col-span-6">
<Input :label="$tc('first_name', 1)" type="text" id="first_name" v-model="contact.first_name" :error="contacts[contactIdx].first_name" />
</div>
<div class="col-span-6">
<Input :label="$tc('last_name', 1)" type="text" id="last_name" v-model="contact.last_name" :error="contacts[contactIdx].last_name" />
</div>
<div class="col-span-3">
<Input :label="$tc('phone', 1)" type="text" id="phone" v-model="contact.phone" :error="contacts[contactIdx].phone" />
</div>
<div class="col-span-6">
<Input :label="$tc('email_address', 1)" type="text" id="email" v-model="contact.email" :error="contacts[contactIdx].email" />
</div>
<div class="col-span-3">
<Input :label="$tc('date_of_birth', 1)" type="date" id="date_of_birth" v-model="contact.date_of_birth" :error="contacts[contactIdx].date_of_birth" />
</div>
<div class="col-span-9">
<Input :label="$tc('function', 1)" type="text" id="function" v-model="contact.function" :error="contacts[contactIdx].function" />
</div>
<div class="col-span-3">
<Select :label="$tc('language', 1)" id="languageOfContact" :options="languageOptions" displayProperty="display_name" valueProperty="name" v-model="contact.language" :error="contacts[contactIdx].language" />
</div>
</div>
</div>
</section>
</fieldset>
<fieldset class="pt-6">
<SubmitButton :label="$tc('create_company', 1)" submittingLabel="Creating company..." />
</fieldset>
</form>
Before the data is send to the backend it is validated
async createCompany() {
try {
await CreateCompanyValidationSchema.validate(this.company, { abortEarly: false });
console.log('all good');
} catch (err) {
console.log(err.inner);
err.inner.forEach((error) => {
this.validationErrors = { ...this.validationErrors, [error.path]: error.message };
});
}
}
I'm using Yup to validate the form. The schema looks like this
export const CreateCompanyValidationSchema = yup.object().shape({
enterprise_number: yup.string(),
business_name: yup.string(),
legal_entity_type: yup.string(),
phone: yup.string().required(),
email: yup.string().required().email(),
language: yup.string().required(),
first_name: yup.string(),
last_name: yup.string(),
date_of_birth: yup.date(),
physical_address: yup.object({
street: yup.string().required(),
number: yup.string().required(),
addition: yup.string(),
zip_city: yup.string().required(),
country: yup.string().required(),
}),
same_billing_address: yup.boolean(),
billing_address: yup.object().when('same_billing_address', {
is: false,
then: yup.object({
street: yup.string().required(),
number: yup.string().required(),
addition: yup.string(),
zip_city: yup.string().required(),
country: yup.string().required(),
}),
}),
contacts: yup.array().of(
yup.object().shape({
gender: yup.string().required().oneOf(['male', 'female', 'other']),
first_name: yup.string().required(),
last_name: yup.string().required(),
phone: yup.string().required(),
email: yup.string().required().email(),
date_of_birth: yup.date().required(),
function: yup.string().required(),
language: yup.string().required().oneOf(['nl', 'fr', 'en']),
})
),
});
The validationErrors data object has a nested object structure (physical_address and billing_address) and a nested array of objects (contacts). The validationErrors object is empty in the beginning. If the nested address fields or the contacts are valid, the validationErrors object will not have any nested properties. But in the form I'm accessing child properties like validationErrors.contacts[contactIdx].phone or validationErrors.billing_address.street. This causes errors because these properties do not exist. What is the best approach to counter this? I'm looking for a reusable solution for multiple forms with this structure.