From 1a11a27fd68663973a7cb539c6d40db7d12ceaf3 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Sun, 28 Apr 2024 22:24:09 +0200 Subject: [PATCH] fix: subfield errors should be consider validated based on its parent (#607) --- packages/conform-dom/form.ts | 7 ++++++- playground/app/components.tsx | 18 ++++++++++++++--- playground/app/routes/collection.tsx | 17 ++++++++++++++-- tests/integrations/collection.spec.ts | 28 +++++++++++++++++++++++---- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/packages/conform-dom/form.ts b/packages/conform-dom/form.ts index f6590643..022fee8d 100644 --- a/packages/conform-dom/form.ts +++ b/packages/conform-dom/form.ts @@ -410,9 +410,14 @@ function handleIntent( } } + const validatedFields = fields?.filter((name) => meta.validated[name]) ?? []; + meta.error = Object.entries(meta.error).reduce>( (result, [name, error]) => { - if (meta.validated[name]) { + if ( + meta.validated[name] || + validatedFields.some((field) => isPrefix(name, field)) + ) { result[name] = error; } diff --git a/playground/app/components.tsx b/playground/app/components.tsx index 9de84307..cdeb85f4 100644 --- a/playground/app/components.tsx +++ b/playground/app/components.tsx @@ -80,10 +80,22 @@ interface FieldProps { label: string; inline?: boolean; meta?: FieldMetadata; + allErrors?: boolean; children: ReactNode; } -export function Field({ label, inline, meta, children }: FieldProps) { +export function Field({ + label, + inline, + meta, + allErrors, + children, +}: FieldProps) { + const errors = + meta && allErrors + ? Object.values(meta.allErrors).flat() + : meta?.errors ?? []; + return (
- {!meta?.errors?.length ? ( + {errors.length === 0 ? (

) : ( - meta.errors.map((message) => ( + errors.map((message) => (

{message}

diff --git a/playground/app/routes/collection.tsx b/playground/app/routes/collection.tsx index 29f8cb9e..0c77e4f5 100644 --- a/playground/app/routes/collection.tsx +++ b/playground/app/routes/collection.tsx @@ -7,7 +7,20 @@ import { Playground, Field } from '~/components'; const schema = z.object({ singleChoice: z.string({ required_error: 'Required' }), - multipleChoice: z.string().array().min(1, 'Required'), + multipleChoice: z + .enum(['a', 'b', 'c'], { + errorMap(issue, ctx) { + if (issue.code === 'invalid_enum_value') { + return { message: 'Invalid' }; + } + + return { + message: ctx.defaultError, + }; + }, + }) + .array() + .min(1, 'Required'), }); export async function loader({ request }: LoaderArgs) { @@ -51,7 +64,7 @@ export default function Example() { ))} - + {getCollectionProps(fields.multipleChoice, { type: 'checkbox', options: ['a', 'b', 'c', 'd'], diff --git a/tests/integrations/collection.spec.ts b/tests/integrations/collection.spec.ts index 88028de4..9182f1ae 100644 --- a/tests/integrations/collection.spec.ts +++ b/tests/integrations/collection.spec.ts @@ -1,7 +1,12 @@ import { type Page, test, expect } from '@playwright/test'; import { getPlayground } from '../helpers'; -async function runValidationScenario(page: Page) { +async function runTest( + page: Page, + options: { + noClientValidate?: boolean; + } = {}, +) { const playground = getPlayground(page); await playground.submit.click(); @@ -13,6 +18,17 @@ async function runValidationScenario(page: Page) { await expect(playground.error).toHaveText(['', 'Required']); + if (!options.noClientValidate) { + const invalidOption = playground.container.getByLabel('D'); + + await invalidOption.click(); + + await expect(playground.error).toHaveText(['', 'Invalid']); + + // Uncheck it to remove the error + await invalidOption.click(); + } + await playground.container.getByLabel('C').click(); await playground.submit.click(); @@ -43,12 +59,14 @@ async function runValidationScenario(page: Page) { test.describe('With JS', () => { test('Client Validation', async ({ page }) => { await page.goto('/collection'); - await runValidationScenario(page); + await runTest(page); }); test('Server Validation', async ({ page }) => { await page.goto('/collection?noClientValidate=yes'); - await runValidationScenario(page); + await runTest(page, { + noClientValidate: true, + }); }); test('Form reset', async ({ page }) => { @@ -69,6 +87,8 @@ test.describe('No JS', () => { test('Validation', async ({ page }) => { await page.goto('/collection'); - await runValidationScenario(page); + await runTest(page, { + noClientValidate: true, + }); }); });