0

I'm having somewhat of an issue using Zod form resolver.

Using PrimeVue forms, I have validate-on-value-update activated:

<Form
   v-slot="$form"
   :initial-values="form.data()"
   :resolver="resolver"
   :validate-on-value-update="true"
   @submit="() => submitCreate(store().url)"
>

For the most part, it's working as intended, but when it comes to fields that are dependent on other fields, that's where the issue comes in.

I'm building a customer form. There's an opt-out field for both email and phone number.

Currently, if I hit submit, it runs validation on all fields, except those in superRefine() (phone and email).

Once I handle those initial invalid fields and submit again, it then runs the superRefine validation for the phone and email.

First, I'd like to solve that. I would like the validation to all occur simultaneously so the user doesn't have to attempt submission multiple times in order to see all validation errors.

Finally, in an example when the user omits both the phone && email & then proceeds to click the related opt-out field, the validation errors are only removed after they've selected opt-out for both email and phone, instead of being removed at the time that they click either phone opt out or email opt out.

Now, I understand that obviously if they're going to be opting out of both, both opt out boxes would have to be clicked, but I'm just thinking in terms of UX, I feel like the validation error should be removed for each field immediately once they click the related opt-out checkbox.

Here is my resolver schema:

import { z } from 'zod'

// keep in sync with your ReferralType in customer.d.ts
export const ReferralTypeEnum = z.enum([
    'realtor',
    'friend_family',
    'internet_search',
    'repeat',
    'other',
], { error: () => ({ message: 'Referral type is required.' })});

// const digitsOnly = (s: string | null | undefined) => (s ?? '').replace(/\D+/g, '')
// const normalizePhone = (s: string | null | undefined) => digitsOnly(s).slice(-11) // tweak as needed

export const CustomerFormSchema = z.object({

    first_name:     z.string().min(1, 'First name is required.'),
    last_name:      z.string().min(1, 'Last name is required.'),

    phone_1:        z.nullable(z.string().min(13, 'Phone number is invalid.')),
    phone_2:        z.nullable(z.string().min(13, 'Phone number is invalid.')),
    phone_opt_out:  z.boolean().default(false),

    email_1:        z.nullable(z.string().optional().default('')),
    email_2:        z.nullable(z.string().optional().default('')),
    email_opt_out:  z.boolean(),

    is_realtor:     z.boolean('Please indicate if customer is a realtor.'),

    contact_email:  z.boolean().default(false),
    contact_call:   z.boolean().default(false),
    contact_text:   z.boolean().default(false),
    no_contact:     z.boolean().default(false),

    referral_type:  ReferralTypeEnum,
    company_name:   z.nullable(z.string().optional().default('')),

    avatar_url:     z.string().default("images/avatars/default.svg"),
})

// cross-field rules
export const CustomerFormStrictSchema = CustomerFormSchema.superRefine((v, ctx) => {

    if (!v.phone_opt_out) {
        if (!v.phone_1) {
            ctx.addIssue({
                code: 'custom',
                path: ['phone_1'],
                message: 'Phone is required when opt-out is false.'
            })
        }
    }

    if (!v.email_opt_out) {
        if (!v.email_1) {
            ctx.addIssue({
                code: 'custom',
                path: ['email_1'],
                message: 'Email is required when opt-out is false.'
            })
        }
    }

})

export type CustomerFormZ = z.infer<typeof CustomerFormStrictSchema>

And the composable for the form:

import { useForm } from "@inertiajs/vue3"
import type { FormResolverOptions } from "@primevue/forms"
import { zodResolver } from "@primevue/forms/resolvers/zod"

import type { CustomerForm } from "@/types/customer"
import { CustomerFormStrictSchema } from "@/schemas/customer"

export function useCustomerForm(initial?: Partial<CustomerForm>) {
  const form = useForm<CustomerForm>({
    first_name: '',
    last_name: '',
    phone_1: null,
    phone_2: null,
    phone_opt_out: false,

    email_1: null,
    email_2: null,
    email_opt_out: false,

    is_realtor: null,

    contact_email: false,
    contact_call: false,
    contact_text: false,
    no_contact: false,

    referral_type: null,
    company_name: null,
    avatar_url: "images/avatars/default.svg",
    ...(initial ?? {}),
  })

  const baseResolver = zodResolver(CustomerFormStrictSchema)

  const resolver = async (e: FormResolverOptions) => {
    return baseResolver(e) // { values, errors } from Zod, no extra logic needed
  }

  function submitCreate(url: string) {
    form.post(url, { preserveScroll: true, onError: (errs) => {console.log(errs)} })
  }

  function submitUpdate(url: string) {
    form.put(url, { preserveScroll: true })
  }

  return { form, resolver, submitCreate, submitUpdate }
}

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.