From 217af69f27949ca04119db1ea4f5dee8d5debe9e Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Tue, 18 Jul 2023 21:29:23 +0200 Subject: [PATCH] feat(conform-react,conform-zod,conform-yup)!: multiple errors by default (#228) --- packages/conform-react/hooks.ts | 17 +----- packages/conform-yup/index.ts | 31 +---------- packages/conform-zod/index.ts | 31 +---------- playground/app/routes/async-validation.tsx | 19 +++++-- playground/app/routes/multiple-errors.tsx | 6 --- playground/app/routes/validate-constraint.tsx | 8 +-- tests/conform-yup.spec.ts | 28 +--------- tests/conform-zod.spec.ts | 54 +------------------ .../integrations/validate-constraint.spec.ts | 40 +++++++++++--- 9 files changed, 55 insertions(+), 179 deletions(-) diff --git a/packages/conform-react/hooks.ts b/packages/conform-react/hooks.ts index 980f6713..de0f16e5 100644 --- a/packages/conform-react/hooks.ts +++ b/packages/conform-react/hooks.ts @@ -1052,15 +1052,6 @@ export function validateConstraint(options: { context: { formData: FormData; attributeValue: string }, ) => boolean >; - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; formatMessages?: ({ name, validity, @@ -1138,15 +1129,9 @@ export function validateConstraint(options: { constraint, defaultErrors: getDefaultErrors(element.validity, constraint), }); - const shouldAcceptMultipleErrors = - options?.acceptMultipleErrors?.({ - name, - payload, - intent, - }) ?? false; if (errors.length > 0) { - error[name] = shouldAcceptMultipleErrors ? errors : errors[0]; + error[name] = errors; } } } diff --git a/packages/conform-yup/index.ts b/packages/conform-yup/index.ts index c8ecaf45..d4bf8777 100644 --- a/packages/conform-yup/index.ts +++ b/packages/conform-yup/index.ts @@ -91,15 +91,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async?: false; }, ): Submission>; @@ -107,15 +98,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async: true; }, ): Promise>>; @@ -123,15 +105,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async?: boolean; }, ): @@ -153,9 +126,7 @@ export function parse( if (typeof result[name] === 'undefined') { result[name] = e.message; - } else if ( - config.acceptMultipleErrors?.({ name, intent, payload }) - ) { + } else { result[name] = ([] as string[]).concat( result[name], e.message, diff --git a/packages/conform-zod/index.ts b/packages/conform-zod/index.ts index 71a9622c..8c881a2a 100644 --- a/packages/conform-zod/index.ts +++ b/packages/conform-zod/index.ts @@ -174,15 +174,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async?: false; errorMap?: z.ZodErrorMap; stripEmptyValue?: boolean; @@ -192,15 +183,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async: true; errorMap?: z.ZodErrorMap; stripEmptyValue?: boolean; @@ -210,15 +192,6 @@ export function parse( payload: FormData | URLSearchParams, config: { schema: Schema | ((intent: string) => Schema); - acceptMultipleErrors?: ({ - name, - intent, - payload, - }: { - name: string; - intent: string; - payload: Record; - }) => boolean; async?: boolean; errorMap?: z.ZodErrorMap; stripEmptyValue?: boolean; @@ -249,9 +222,7 @@ export function parse( if (typeof result[name] === 'undefined') { result[name] = e.message; - } else if ( - config.acceptMultipleErrors?.({ name, intent, payload }) - ) { + } else { result[name] = ([] as string[]).concat(result[name], e.message); } diff --git a/playground/app/routes/async-validation.tsx b/playground/app/routes/async-validation.tsx index 896b60bb..48e03c0e 100644 --- a/playground/app/routes/async-validation.tsx +++ b/playground/app/routes/async-validation.tsx @@ -15,14 +15,23 @@ function createSchema( return z.object({ email: z .string({ required_error: 'Email is required' }) - .email('Email is invalid') - .superRefine((email, ctx) => - refine(ctx, { + .superRefine((email, ctx) => { + const result = z.string().email().safeParse(email); + + if (!result.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Email is invalid', + }); + return; + } + + return refine(ctx, { validate: () => constraints.isEmailUnique?.(email), when: intent === 'validate/email' || intent === 'submit', message: 'Email is already used', - }), - ), + }); + }), title: z .string({ required_error: 'Title is required' }) .max(20, 'Title is too long'), diff --git a/playground/app/routes/multiple-errors.tsx b/playground/app/routes/multiple-errors.tsx index 07cea915..ecb32462 100644 --- a/playground/app/routes/multiple-errors.tsx +++ b/playground/app/routes/multiple-errors.tsx @@ -40,9 +40,6 @@ function parseForm(formData: FormData, validator: string | null) { (username) => !username || username.match(/[0-9]/) !== null, ), }), - acceptMultipleErrors({ name }) { - return name === 'username'; - }, }); } case 'zod': { @@ -61,9 +58,6 @@ function parseForm(formData: FormData, validator: string | null) { ) .refine((username) => username.match(/[0-9]/), 'At least 1 number'), }), - acceptMultipleErrors({ name }) { - return name === 'username'; - }, }); } default: { diff --git a/playground/app/routes/validate-constraint.tsx b/playground/app/routes/validate-constraint.tsx index 05937660..8bfa2082 100644 --- a/playground/app/routes/validate-constraint.tsx +++ b/playground/app/routes/validate-constraint.tsx @@ -18,8 +18,6 @@ export async function loader({ request }: LoaderArgs) { const url = new URL(request.url); return { - multiplePasswordError: - url.searchParams.get('multiplePasswordError') === 'yes', enableCustomMessage: url.searchParams.get('enableCustomMessage') === 'yes', }; } @@ -28,8 +26,7 @@ export default function Example() { const [lastSubmission, setLastSubmission] = useState< Submission | undefined >(); - const { multiplePasswordError, enableCustomMessage } = - useLoaderData(); + const { enableCustomMessage } = useLoaderData(); const [form, { email, password, confirmPassword }] = useForm({ fallbackNative: true, shouldRevalidate: 'onInput', @@ -41,9 +38,6 @@ export default function Example() { return value === formData.get(attributeValue); }, }, - acceptMultipleErrors({ name }) { - return multiplePasswordError && name === 'password'; - }, formatMessages({ name, defaultErrors }) { if (!enableCustomMessage) { return defaultErrors; diff --git a/tests/conform-yup.spec.ts b/tests/conform-yup.spec.ts index c73ab231..65f3330c 100644 --- a/tests/conform-yup.spec.ts +++ b/tests/conform-yup.spec.ts @@ -62,8 +62,8 @@ test.describe('conform-yup', () => { list: [{ key: '' }], }; const error = { - text: 'min', - tag: 'required', + text: ['min', 'regex'], + tag: ['required', 'invalid'], number: 'max', timestamp: 'min', 'options[1]': 'invalid', @@ -116,29 +116,5 @@ test.describe('conform-yup', () => { payload, error, }); - expect( - parse(formData, { schema, acceptMultipleErrors: () => true }), - ).toEqual({ - intent: 'submit', - payload, - error: { - ...error, - text: ['min', 'regex'], - tag: ['required', 'invalid'], - }, - }); - expect( - parse(formData, { - schema, - acceptMultipleErrors: ({ name }) => name === 'tag', - }), - ).toEqual({ - intent: 'submit', - payload, - error: { - ...error, - tag: ['required', 'invalid'], - }, - }); }); }); diff --git a/tests/conform-zod.spec.ts b/tests/conform-zod.spec.ts index a614688b..55efb2cf 100644 --- a/tests/conform-zod.spec.ts +++ b/tests/conform-zod.spec.ts @@ -212,7 +212,7 @@ test.describe('conform-zod', () => { list: [{ key: undefined }], }; const error = { - text: 'min', + text: ['min', 'regex', 'refine'], number: 'step', timestamp: 'min', options: 'min', @@ -230,31 +230,6 @@ test.describe('conform-zod', () => { payload, error, }); - expect( - parse(formData, { - schema, - stripEmptyValue: true, - acceptMultipleErrors: () => false, - }), - ).toEqual({ - intent: 'submit', - payload, - error, - }); - expect( - parse(formData, { - schema, - stripEmptyValue: true, - acceptMultipleErrors: () => true, - }), - ).toEqual({ - intent: 'submit', - payload, - error: { - ...error, - text: ['min', 'regex', 'refine'], - }, - }); }); test('parse without empty value stripped', () => { @@ -282,7 +257,7 @@ test.describe('conform-zod', () => { list: [{ key: '' }], }; const error = { - text: 'min', + text: ['min', 'regex', 'refine'], number: 'step', timestamp: 'min', options: 'min', @@ -301,31 +276,6 @@ test.describe('conform-zod', () => { payload, error, }); - expect( - parse(formData, { - schema, - stripEmptyValue: false, - acceptMultipleErrors: () => false, - }), - ).toEqual({ - intent: 'submit', - payload, - error, - }); - expect( - parse(formData, { - schema, - stripEmptyValue: false, - acceptMultipleErrors: () => true, - }), - ).toEqual({ - intent: 'submit', - payload, - error: { - ...error, - text: ['min', 'regex', 'refine'], - }, - }); }); test('parse with errorMap', () => { diff --git a/tests/integrations/validate-constraint.spec.ts b/tests/integrations/validate-constraint.spec.ts index 2c65ac48..d8762070 100644 --- a/tests/integrations/validate-constraint.spec.ts +++ b/tests/integrations/validate-constraint.spec.ts @@ -25,10 +25,20 @@ test('Basic usage with constraint', async ({ page }) => { ]); await email.type('a'); - await expect(playground.error).toHaveText(['type', 'required', 'required']); + await expect(playground.error).toHaveText([ + 'type', + 'pattern', + 'required', + 'required', + ]); await email.type('@'); - await expect(playground.error).toHaveText(['type', 'required', 'required']); + await expect(playground.error).toHaveText([ + 'type', + 'pattern', + 'required', + 'required', + ]); await email.type('e'); await expect(playground.error).toHaveText([ @@ -38,19 +48,35 @@ test('Basic usage with constraint', async ({ page }) => { ]); await email.type('xample.'); - await expect(playground.error).toHaveText(['type', 'required', 'required']); + await expect(playground.error).toHaveText([ + 'type', + 'pattern', + 'required', + 'required', + ]); await email.type('com'); await expect(playground.error).toHaveText(['', 'required', 'required']); await password.type('H'); - await expect(playground.error).toHaveText(['', 'minLength', 'required']); + await expect(playground.error).toHaveText([ + '', + 'minLength', + 'pattern', + 'required', + 'match', + ]); await password.type('ello'); - await expect(playground.error).toHaveText(['', 'pattern', 'required']); + await expect(playground.error).toHaveText([ + '', + 'pattern', + 'required', + 'match', + ]); await password.type('123'); - await expect(playground.error).toHaveText(['', '', 'required']); + await expect(playground.error).toHaveText(['', '', 'required', 'match']); await confirmPassword.type('Hello123'); await expect(playground.error).toHaveText(['', '', '']); @@ -82,7 +108,7 @@ test('Basic usage with constraint', async ({ page }) => { }); test('Mutliple error support', async ({ page }) => { - await page.goto('/validate-constraint?multiplePasswordError=yes'); + await page.goto('/validate-constraint'); const playground = getPlayground(page); const { email, password, confirmPassword } = getFieldset(