T
TanStack•6mo ago
conscious-sapphire

formOpts.onSubmit example

Does anyone have an example of how to move their onSubmit() to the formOpts? I would need to pass quite a bit of state and I can't figure it out. It would be really nice if I could set new state for tanstack store for postError to use in the onSubmit as well so I could then use it in A form component without passing it as props.
11 Replies
dependent-tan
dependent-tan•6mo ago
import { formOptions } from '@tanstack/react-form';

export const peopleFormOpts = formOptions({
defaultValues: {
fullName: '',
email: '',
phone: '',
address: {
line1: '',
line2: '',
city: '',
state: '',
zip: '',
},
emergencyContact: {
fullName: '',
phone: '',
},
},
onSubmit: ({ value }) => {
alert(JSON.stringify(value, null, 2))
},
});
import { formOptions } from '@tanstack/react-form';

export const peopleFormOpts = formOptions({
defaultValues: {
fullName: '',
email: '',
phone: '',
address: {
line1: '',
line2: '',
city: '',
state: '',
zip: '',
},
emergencyContact: {
fullName: '',
phone: '',
},
},
onSubmit: ({ value }) => {
alert(JSON.stringify(value, null, 2))
},
});
works just fine on the Form Composition example if it is not overridden on the form itself. ... I think we need some more information or code to get the problem. Can you write up some code that shows what you're trying to achieve?
conscious-sapphire
conscious-sapphireOP•6mo ago
I almost got it. I just don't understand how to use form.reset() with the onSubmit declared outside of the form.
// src/blocks/tanstackForm/form-options.tsx
'use client'

import { formOptions } from '@tanstack/react-form'
import type { Form } from '@/payload-types'
import type { Dispatch, SetStateAction, useState } from 'react'
import { getClientSideURL } from '@/utilities/getURL'
import { useRouter } from 'next/navigation'

export type FormField = NonNullable<Form['fields']>[number]
export type DefaultValues = Record<string, string | number | boolean | any[] | undefined>

export const useFormOpts = ({
payloadForm,
setPostError,
}: {
payloadForm: Form | string
setPostError: Dispatch<
SetStateAction<
| {
message: string
status?: string
}
| undefined
>
>
}) => {
const { confirmationType, fields, title, redirect } =
typeof payloadForm !== 'string' ? payloadForm : {}
const router = useRouter()
const defaultValues = getDefaultValues(fields)

return formOptions({
defaultValues,
onSubmit: async ({ value: data }) => {
setPostError(undefined)

try {
const req = await fetch(`${getClientSideURL()}/api/form-submissions`, {
// POST request
})

const res = await req.json()

if (req.status >= 400) {
setPostError({
message: res.errors?.[0]?.message || 'Internal Server Error',
status: res.status,
})

return
}

if (confirmationType === 'redirect' && redirect) {
// redirect logic
}

form.reset() // type error as form does not exist here
} catch (err) {
console.warn(err)
setPostError({
message: 'Something went wrong.',
})
}
},
})
}

const getDefaultValues = (fields: Form['fields']) => {
// generate default values logic
}
// src/blocks/tanstackForm/form-options.tsx
'use client'

import { formOptions } from '@tanstack/react-form'
import type { Form } from '@/payload-types'
import type { Dispatch, SetStateAction, useState } from 'react'
import { getClientSideURL } from '@/utilities/getURL'
import { useRouter } from 'next/navigation'

export type FormField = NonNullable<Form['fields']>[number]
export type DefaultValues = Record<string, string | number | boolean | any[] | undefined>

export const useFormOpts = ({
payloadForm,
setPostError,
}: {
payloadForm: Form | string
setPostError: Dispatch<
SetStateAction<
| {
message: string
status?: string
}
| undefined
>
>
}) => {
const { confirmationType, fields, title, redirect } =
typeof payloadForm !== 'string' ? payloadForm : {}
const router = useRouter()
const defaultValues = getDefaultValues(fields)

return formOptions({
defaultValues,
onSubmit: async ({ value: data }) => {
setPostError(undefined)

try {
const req = await fetch(`${getClientSideURL()}/api/form-submissions`, {
// POST request
})

const res = await req.json()

if (req.status >= 400) {
setPostError({
message: res.errors?.[0]?.message || 'Internal Server Error',
status: res.status,
})

return
}

if (confirmationType === 'redirect' && redirect) {
// redirect logic
}

form.reset() // type error as form does not exist here
} catch (err) {
console.warn(err)
setPostError({
message: 'Something went wrong.',
})
}
},
})
}

const getDefaultValues = (fields: Form['fields']) => {
// generate default values logic
}
dependent-tan
dependent-tan•6mo ago
onSubmit also has a formApi: AnyFormApi prop. I haven't tested it, but this should work? Typescript certainly likes it.
formOptions({
defaultValues,
onSubmit: ({ value, formApi }) => {
// (...)
formApi.reset()
},
})
formOptions({
defaultValues,
onSubmit: ({ value, formApi }) => {
// (...)
formApi.reset()
},
})
conscious-sapphire
conscious-sapphireOP•6mo ago
Thank you so much, I was looking everywhere except there, lol. Thanks again!!
dependent-tan
dependent-tan•6mo ago
No problem, I was about to suggest passing in the form with that type (which would have been super weird) when I got the idea to look at what is actually passed besides value 😄
rising-crimson
rising-crimson•6mo ago
this actually made me think, could TSF also export a higher order hook that contains the form as an arg in it, like we have with withForm for higher order components
conscious-sapphire
conscious-sapphireOP•6mo ago
I think Crutchcorn said somewhere he'd leave that up to us if we wanted to put it in a context. I have to pass the defaultValues everywhere because I generate them from a CMS. I wish I could generate them right in the withForm(), but instead I have to wrap it in a React Component, and also pass the form as any, but it's not too bad.
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<Card className="@container">
<CardContent className="grid grid-cols-1 gap-4 @lg:grid-cols-2 p-6 auto-cols-fr">
{fields?.map((field) => (
<RenderFields
key={field.id}
field={field}
defaultValues={defaultValues}
form={form}
/>
))}
</CardContent>
<CardFooter>
<form.AppForm>
<form.SubscribeButton label={submitButtonLabel ?? 'Submit'} />
</form.AppForm>
</CardFooter>
</Card>
</form>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<Card className="@container">
<CardContent className="grid grid-cols-1 gap-4 @lg:grid-cols-2 p-6 auto-cols-fr">
{fields?.map((field) => (
<RenderFields
key={field.id}
field={field}
defaultValues={defaultValues}
form={form}
/>
))}
</CardContent>
<CardFooter>
<form.AppForm>
<form.SubscribeButton label={submitButtonLabel ?? 'Submit'} />
</form.AppForm>
</CardFooter>
</Card>
</form>
export const RenderFields = ({
field,
defaultValues,
form,
}: {
field: FormField
defaultValues: DefaultValues
form: any
}) => {
const FieldComponent = withForm({
defaultValues,
props: {
field,
},
render: function Render({ form, field }) {
switch (field.blockType) {
case 'text':
return (
<form.AppField
key={field.id}
name={field.name}
>
{(formField) => <formField.TextField {...field} />}
</form.AppField>
)
// ...
default:
return <></>
}
},
})
return <FieldComponent form={form} field={field} />
}
export const RenderFields = ({
field,
defaultValues,
form,
}: {
field: FormField
defaultValues: DefaultValues
form: any
}) => {
const FieldComponent = withForm({
defaultValues,
props: {
field,
},
render: function Render({ form, field }) {
switch (field.blockType) {
case 'text':
return (
<form.AppField
key={field.id}
name={field.name}
>
{(formField) => <formField.TextField {...field} />}
</form.AppField>
)
// ...
default:
return <></>
}
},
})
return <FieldComponent form={form} field={field} />
}
dependent-tan
dependent-tan•6mo ago
Why not AnyFormApi to at least make it a form type?
conscious-sapphire
conscious-sapphireOP•6mo ago
I knew you had a Type I could use. Thanks again! AnyFormApi actually causes a type error when passing it into a withForm component. But AnyFormApi | any works and gives me some form auto complete.
dependent-tan
dependent-tan•6mo ago
Dang it, I didn't think that would bite even when any is involved as well 😅 .
conscious-sapphire
conscious-sapphireOP•6mo ago
This solved my problem with needing to pass the form other components.
export const useDynamicForm = ({ payloadForm, setPostError }: Props) => {
const { defaultValues, onSubmit } = useFormOpts({
payloadForm,
setPostError,
})
const form = useAppForm({
defaultValues,
onSubmit,
})
return { form, defaultValues }
}

export type DynamicFormType = ReturnType<typeof useDynamicForm>['form']
export const useDynamicForm = ({ payloadForm, setPostError }: Props) => {
const { defaultValues, onSubmit } = useFormOpts({
payloadForm,
setPostError,
})
const form = useAppForm({
defaultValues,
onSubmit,
})
return { form, defaultValues }
}

export type DynamicFormType = ReturnType<typeof useDynamicForm>['form']

Did you find this page helpful?