diff --git a/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx b/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx new file mode 100644 index 0000000000..c33ff93686 --- /dev/null +++ b/e2e/start/basic/app/routes/-server-fns/serialize-formdata-fn-call.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import { createServerFn } from '@tanstack/start' + +const testValues = { + name: 'Sean', + age: 25, + __adder: 1, +} + +export const greetUser = createServerFn() + .validator((data: unknown) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + const age = data.get('age') + + if (!name || !age) { + throw new Error('Name and age are required') + } + + return { + name: name.toString(), + age: parseInt(age.toString(), 10), + } + }) + .handler(async ({ data: { name, age } }) => { + return `Hello, ${name}! You are ${age + testValues.__adder} years old.` + }) + +// Usage +export function SerializeFormDataFnCall() { + const [formDataResult, setFormDataResult] = React.useState({}) + + return ( +
+

Serialize FormData Fn POST Call

+
+ It should return{' '} + +
+            Hello, {testValues.name}! You are{' '}
+            {testValues.age + testValues.__adder} years old.
+          
+
+
+
{ + evt.preventDefault() + const data = new FormData(evt.currentTarget) + greetUser({ data }).then(setFormDataResult) + }} + > + + + +
+
+
+          {JSON.stringify(formDataResult)}
+        
+
+
+ ) +} diff --git a/e2e/start/basic/app/routes/server-fns.tsx b/e2e/start/basic/app/routes/server-fns.tsx index 21939570b9..b971473310 100644 --- a/e2e/start/basic/app/routes/server-fns.tsx +++ b/e2e/start/basic/app/routes/server-fns.tsx @@ -4,6 +4,7 @@ import { createFileRoute } from '@tanstack/react-router' import { ConsistentServerFnCalls } from './-server-fns/consistent-fn-calls' import { MultipartServerFnCall } from './-server-fns/multipart-formdata-fn-call' import { AllowServerFnReturnNull } from './-server-fns/allow-fn-return-null' +import { SerializeFormDataFnCall } from './-server-fns/serialize-formdata-fn-call' export const Route = createFileRoute('/server-fns')({ component: RouteComponent, @@ -15,6 +16,7 @@ function RouteComponent() { + ) } diff --git a/e2e/start/basic/tests/base.spec.ts b/e2e/start/basic/tests/base.spec.ts index 3b6158ed09..1a4ac7646d 100644 --- a/e2e/start/basic/tests/base.spec.ts +++ b/e2e/start/basic/tests/base.spec.ts @@ -223,3 +223,23 @@ test('Server function can return null for GET and POST calls', async ({ page.getByTestId('allow_return_null_postFn-response'), ).toContainText(JSON.stringify(null)) }) + +test('Server function can correctly send and receive FormData', async ({ + page, +}) => { + await page.goto('/server-fns') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-serialize-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-serialize-formdata-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + await expect( + page.getByTestId('serialize-formdata-form-response'), + ).toContainText(expected) +}) diff --git a/packages/react-router/src/transformer.ts b/packages/react-router/src/transformer.ts index f58f03bb59..3fcd66d221 100644 --- a/packages/react-router/src/transformer.ts +++ b/packages/react-router/src/transformer.ts @@ -33,7 +33,7 @@ export const defaultTransformer: RouterTransformer = { return val }), encode: (value: any) => { - // When encodign, dive first + // When encoding, dive first if (Array.isArray(value)) { return value.map((v) => defaultTransformer.encode(v)) } @@ -125,6 +125,28 @@ const transformers = [ // From (v) => Object.assign(new Error(v.message), v), ), + createTransformer( + // Key + 'formData', + // Check + (v) => v instanceof FormData, + // To + (v: FormData) => { + const entries: Record = {} + v.forEach((value, key) => { + entries[key] = value + }) + return entries + }, + // From + (v) => { + const formData = new FormData() + Object.entries(v).forEach(([key, value]) => { + formData.append(key, value as string | Blob) + }) + return formData + }, + ), ] as const export type TransformerStringify = T extends TSerializable