Vue:访问错误对象中可能存在的嵌套属性的最佳方法

Posted

技术标签:

【中文标题】Vue:访问错误对象中可能存在的嵌套属性的最佳方法【英文标题】:Vue: Best approach to access possible existing nested properties in error object 【发布时间】:2022-01-17 07:02:47 【问题描述】:

我正在寻找一种良好且可重用的方法来访问没有 typeErrors 的错误对象内可能存在的嵌套属性(嵌套对象和对象数组)。

我有一个createCompany 表单/页面,其中包含以下数据

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: ,
    

表单本身是这样的

<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>

在数据发送到后端之前对其进行验证

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 ;
        );
    

我正在使用Yup 来验证表单。架构如下所示

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']),
        )
    ),
);

validationErrors 数据对象具有嵌套对象结构(physical_addressbilling_address)和嵌套对象数组(contacts)。 validationErrors 对象一开始是空的。如果嵌套地址字段或联系人有效,则validationErrors 对象将没有任何嵌套属性。但在表单中,我正在访问 validationErrors.contacts[contactIdx].phonevalidationErrors.billing_address.street 等子属性。这会导致错误,因为这些属性不存在。应对这种情况的最佳方法是什么?我正在寻找具有这种结构的多种表单的可重用解决方案。

【问题讨论】:

这是 Vue 2 应用程序,不是吗?您可以使用 ?. 可选导航运算符,但它在 Vue 2 模板中不起作用。可能不是直接访问validationErrors,而是使用包装Lodash get 或类似的安全导航实用程序的方法 这是一个 vue 3 应用程序,我们希望避免使用 Lodash。使用?.validationErrors.billing_address.street 的一个选项,但是联系人呢?我认为这是不可能的validationErrors.contacts?[contactIdx].phone Lodash 是最受欢迎的。应该有微小的替代品。 我会检查 lodash get 函数,看看写一个本机可重用函数 实现真的很简单,如果性能不优先,需要几行。 【参考方案1】:

一种选择是使用短路安全访问嵌套属性,前提是它们存在。

if ( validationErrors && validationErrors.billing_address ) 
    console.log(validationErrors.billing_address.street)

if 语句仅在 validationErrors.billing_address 已定义时为真,但在 validationErrors 未定义时不会抛出错误。

如果您认为这种模式过于丑陋和重复,您可能会对使用库来帮助您访问嵌套属性感兴趣。 https://lodash.com/docs/#get

【讨论】:

【参考方案2】:

在 Vue 3 中,可以直接在模板中使用可选链(安全导航)运算符来访问不存在的键:

:error="contacts?.[contactIdx]?.first_name"

在 Vue 2 中,这需要将此代码移动到一个方法中,并可能使用字符串指定嵌套属性的路径,例如使用 Lodash get 或类似的辅助函数,它还允许指定不存在的默认值键。

【讨论】:

以上是关于Vue:访问错误对象中可能存在的嵌套属性的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

测试嵌套 JavaScript 对象键是不是存在

测试嵌套 JavaScript 对象键是不是存在

解决打字稿错误的最佳方法 - 类型上不存在属性

将计算属性嵌套在一个对象中以进行代码维护 (Vue)

无法从动态导入访问对象属性。 Vue.js + ts

Graphql Django 获取嵌套对象属性的最佳方法