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