Estoy buscando un enfoque bueno y reutilizable para acceder a posibles propiedades anidadas existentes (objeto anidado y matriz de objetos) dentro de un objeto de error sin typeErrors.
Tengo un formulario/página createCompany con los siguientes datos
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: {}, } }El formulario en sí se ve así
<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>Antes de que los datos se envíen al backend, se validan
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 }; }); } } Estoy usando Yup para validar el formulario. El esquema se ve así
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']), }) ), }); El objeto de datos validationErrors tiene una estructura de objeto physical_address ( dirección_física y billing_address ) y una matriz anidada de objetos ( contacts ). El objeto validationErrors está vacío al principio. Si los campos de dirección anidados o los contactos son válidos, el objeto validationErrors no tendrá ninguna propiedad anidada. Pero en el formulario estoy accediendo a propiedades secundarias como validationErrors.contacts[contactIdx].phone o validationErrors.billing_address.street . Esto provoca errores porque estas propiedades no existen. ¿Cuál es el mejor enfoque para contrarrestar esto? Estoy buscando una solución reutilizable para múltiples formularios con esta estructura.
Una opción es utilizar un cortocircuito para acceder de forma segura a las propiedades anidadas, solo si existen.
if ( validationErrors && validationErrors.billing_address ) { console.log(validationErrors.billing_address.street) } La declaración if solo es verdadera si la validationErrors.billing_address está definida, pero no generará un error si la validationErrors no está definida.
Si cree que este patrón es demasiado feo y repetitivo, es posible que le interese usar una biblioteca para ayudarlo a acceder a las propiedades anidadas. https://lodash.com/docs/#get
En Vue 3, el operador de encadenamiento opcional (navegación segura) se puede usar directamente en plantillas para acceder a claves inexistentes:
:error="contacts?.[contactIdx]?.first_name" En Vue 2, esto requiere mover este código a un método y probablemente especificar una ruta a la propiedad anidada con cadenas, por ejemplo, con Lodash get o una función auxiliar similar, que también permite especificar el valor predeterminado para claves inexistentes.