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()" >
<fieldset >
<header>
<h3 >{{ $tc('general', 1) }}</h3>
</header>
<div >
<div >
<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 >
<Input :label="$tc('business_name', 1)" type="text" id="companyName" v-model="company.business_name" :error="validationErrors.business_name" />
</div>
<div >
<Input :label="$tc('legal_entity_type', 1)" type="text" id="companyType" v-model="company.legal_entity_type" :error="validationErrors.legal_entity_type" />
</div>
<div >
<Input :label="$tc('phone', 1)" type="text" id="phone" v-model="company.phone" :error="validationErrors.phone" />
</div>
<div >
<Input :label="$tc('email_address', 1)" type="text" id="email" v-model="company.email" :error="validationErrors.email" />
</div>
</div>
</fieldset>
<fieldset >
<header>
<h3 >{{ $tc('physical_address', 1) }}</h3>
</header>
<div >
<div >
<Input :label="$tc('street', 1)" type="text" id="street" v-model="company.physical_address.street" :error="validationErrors.physical_address.street" />
</div>
<div >
<Input :label="$tc('number', 1)" type="text" id="number" v-model="company.physical_address.number" :error="validationErrors.physical_address.number" />
</div>
<div >
<Input :label="$tc('addition', 1)" optional type="text" id="addition" v-model="company.physical_address.addition" :error="validationErrors.physical_address.addition" />
</div>
<div >
<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 >
<Input :label="$tc('country', 1)" type="text" id="country" v-model="company.physical_address.country" :error="validationErrors.physical_address.country" />
</div>
</div>
</fieldset>
<fieldset >
<header>
<h3 >{{ $tc('billing_address', 1) }}</h3>
</header>
<div >
<div >
<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 >
<Input :label="$tc('street', 1)" type="text" id="street" v-model="company.billing_address.street" :error="validationErrors.billing_address.street" />
</div>
<div >
<Input :label="$tc('number', 1)" type="text" id="number" v-model="company.billing_address.number" :error="validationErrors.billing_address.number" />
</div>
<div >
<Input :label="$tc('addition', 1)" type="text" id="addition" v-model="company.billing_address.addition" :error="validationErrors.billing_address.addition" />
</div>
<div >
<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 >
<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 >
<div >
<header>
<h3 >{{ $tc('contact', company.contacts.length) }}</h3>
</header>
<button type="button" @click="addContact">{{ $tc('add', 1) }} {{ $tc('contact', 1).toLowerCase() }}</button>
</div>
<section >
<div v-for="(contact, contactIdx) in company.contacts" :key="contactIdx">
<h4 v-show="company.contacts.length > 1" >
{{ $tc('contact', 1) }} {{ contactIdx 1 }} <span @click="deleteContact(contactIdx)" >({{ $tc('delete', 1) }})</span>
</h4>
<div >
<div >
<RadioButtonGroup :label="$tc('gender', 1)" :options="genderOptions" v-model="contact.gender" :error="contacts[contactIdx].gender" />
</div>
<div >
<Input :label="$tc('first_name', 1)" type="text" id="first_name" v-model="contact.first_name" :error="contacts[contactIdx].first_name" />
</div>
<div >
<Input :label="$tc('last_name', 1)" type="text" id="last_name" v-model="contact.last_name" :error="contacts[contactIdx].last_name" />
</div>
<div >
<Input :label="$tc('phone', 1)" type="text" id="phone" v-model="contact.phone" :error="contacts[contactIdx].phone" />
</div>
<div >
<Input :label="$tc('email_address', 1)" type="text" id="email" v-model="contact.email" :error="contacts[contactIdx].email" />
</div>
<div >
<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 >
<Input :label="$tc('function', 1)" type="text" id="function" v-model="contact.function" :error="contacts[contactIdx].function" />
</div>
<div >
<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 >
<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.
CodePudding user response:
In Vue 3, optional chaining (safe navigation) operator can be used directly in templates to access non-existent keys:
:error="contacts?.[contactIdx]?.first_name"
In Vue 2, this requires to move this code to a method and likely specify a path to nested property with strings, for example with Lodash get
or similar helper function, which also allows to specify default value for non-existent keys.
CodePudding user response:
One option is to use short-circuiting to safely access nested properties, only if they exist.
if ( validationErrors && validationErrors.billing_address ) {
console.log(validationErrors.billing_address.street)
}
The if-statement is only true if validationErrors.billing_address
is defined, but it won't throw an error if validationErrors
is undefined.
If you think that this pattern is too ugly and repetitive, you might be interested in using a library to help you access nested properties. https://lodash.com/docs/#get