diff --git a/apps/web/app/routes/examples.tsx b/apps/web/app/routes/examples.tsx index 0df6fed..3928d61 100644 --- a/apps/web/app/routes/examples.tsx +++ b/apps/web/app/routes/examples.tsx @@ -25,6 +25,9 @@ export default function Component() { Field error + + Transform values + Modes onSubmit diff --git a/apps/web/app/routes/examples/actions/transform-values.tsx b/apps/web/app/routes/examples/actions/transform-values.tsx new file mode 100644 index 0000000..639be9a --- /dev/null +++ b/apps/web/app/routes/examples/actions/transform-values.tsx @@ -0,0 +1,70 @@ +import hljs from 'highlight.js/lib/common' +import type { + ActionFunction, + LoaderFunction, + MetaFunction, +} from '@remix-run/node' +import { formAction } from '~/formAction' +import { z } from 'zod' +import Form from '~/ui/form' +import { metaTags } from '~/helpers' +import { makeDomainFunction } from 'domain-functions' +import Example from '~/ui/example' + +const title = 'Transform values' +const description = + 'In this example, we use different schemas for the form and the mutation, transforming the form values before calling the mutation.' + +export const meta: MetaFunction = () => metaTags({ title, description }) + +const code = `const formSchema = z.object({ + firstName: z.string().min(1), + email: z.string().min(1).email(), +}) + +const mutationSchema = formSchema.extend({ + country: z.enum(['BR', 'US']), +}) + +const mutation = makeDomainFunction(mutationSchema)(async (values) => values) + +export const action: ActionFunction = async ({ request }) => + formAction({ + request, + schema: formSchema, + mutation, + transformValues: (values) => ({ ...values, country: 'US' }), + }) + +export default () =>
` + +const formSchema = z.object({ + firstName: z.string().min(1), + email: z.string().min(1).email(), +}) + +const mutationSchema = formSchema.extend({ + country: z.enum(['BR', 'US']), +}) + +export const loader: LoaderFunction = () => ({ + code: hljs.highlight(code, { language: 'ts' }).value, +}) + +const mutation = makeDomainFunction(mutationSchema)(async (values) => values) + +export const action: ActionFunction = async ({ request }) => + formAction({ + request, + schema: formSchema, + mutation, + transformValues: (values) => ({ ...values, country: 'US' }), + }) + +export default function Component() { + return ( + + + + ) +} diff --git a/apps/web/tests/examples/actions/transform-values.spec.ts b/apps/web/tests/examples/actions/transform-values.spec.ts new file mode 100644 index 0000000..aae893d --- /dev/null +++ b/apps/web/tests/examples/actions/transform-values.spec.ts @@ -0,0 +1,104 @@ +import { test, testWithoutJS, expect } from 'tests/setup/tests' + +const route = '/examples/actions/transform-values' + +test('With JS enabled', async ({ example }) => { + const { firstName, email, button, page } = example + + await page.goto(route) + + // Render + await example.expectField(firstName) + await example.expectField(email) + await expect(button).toBeEnabled() + + // Client-side validation + await button.click() + + // Show field errors and focus on the first field + await example.expectError( + firstName, + 'String must contain at least 1 character(s)', + ) + await example.expectError( + email, + 'String must contain at least 1 character(s)', + ) + await expect(firstName.input).toBeFocused() + + // Make first field be valid, focus goes to the second field + await firstName.input.fill('John') + await button.click() + await example.expectValid(firstName) + await expect(email.input).toBeFocused() + + // Try another invalid message + await email.input.fill('john') + await example.expectError(email, 'Invalid email') + + // Make form be valid + await email.input.fill('john@doe.com') + await example.expectValid(email) + + // Submit form + button.click() + await expect(button).toBeDisabled() + + await example.expectData({ + firstName: 'John', + email: 'john@doe.com', + country: 'US', + }) +}) + +testWithoutJS('With JS disabled', async ({ example }) => { + const { firstName, email, button, page } = example + + await page.goto(route) + + // Server-side validation + await button.click() + await page.reload() + + // Show field errors and focus on the first field + await example.expectError( + firstName, + 'String must contain at least 1 character(s)', + ) + + await example.expectErrors( + email, + 'String must contain at least 1 character(s)', + 'Invalid email', + ) + + await example.expectAutoFocus(firstName) + await example.expectNoAutoFocus(email) + + // Make first field be valid, focus goes to the second field + await firstName.input.fill('John') + await button.click() + await page.reload() + await example.expectValid(firstName) + await example.expectNoAutoFocus(firstName) + await example.expectAutoFocus(email) + + // Try another invalid message + await email.input.fill('john') + await button.click() + await page.reload() + await example.expectError(email, 'Invalid email') + + // Make form be valid and test selecting an option + await email.input.fill('john@doe.com') + + // Submit form + await button.click() + await page.reload() + + await example.expectData({ + firstName: 'John', + email: 'john@doe.com', + country: 'US', + }) +}) diff --git a/packages/remix-forms/src/mutations.ts b/packages/remix-forms/src/mutations.ts index 7e04eff..b0c0896 100644 --- a/packages/remix-forms/src/mutations.ts +++ b/packages/remix-forms/src/mutations.ts @@ -30,6 +30,9 @@ type PerformMutationProps = { schema: Schema mutation: DomainFunction environment?: unknown + transformValues?: ( + values: FormValues>, + ) => Record } type FormActionProps = { @@ -60,11 +63,12 @@ async function performMutation({ schema, mutation, environment, + transformValues = (values) => values, }: PerformMutationProps): Promise< PerformMutation, D> > { const values = await getFormValues(request, schema) - const result = await mutation(values, environment) + const result = await mutation(transformValues(values), environment) if (result.success) { return { success: true, data: result.data } @@ -97,6 +101,7 @@ function createFormAction({ schema, mutation, environment, + transformValues, beforeAction, beforeSuccess, successPath, @@ -111,6 +116,7 @@ function createFormAction({ schema, mutation, environment, + transformValues, }) if (result.success) {