Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
187db7b
feat: add createFormGroup
LeCarbonator Apr 30, 2025
5660529
refactor: move createFormGroup to AppForm
LeCarbonator May 1, 2025
072898e
chore: add unit tests for createFormGroup types
LeCarbonator May 1, 2025
9add0bb
chore: add unit test for createFormGroup
LeCarbonator May 1, 2025
6974927
chore: export CreateFormGroupProps
LeCarbonator May 1, 2025
a0238ed
Merge branch 'main' into form-group-api
LeCarbonator May 1, 2025
edd4fcb
add DeepKeysOfType util type
LeCarbonator May 8, 2025
5c7abbf
feat: add initial FormLensApi draft
LeCarbonator May 8, 2025
61c7049
Merge branch 'main' of github.com:TanStack/form into form-group-api
LeCarbonator May 8, 2025
15eecd4
chore: add FormLensApi tests batch
LeCarbonator May 8, 2025
3e99f6f
fix(form-core): fix form.resetField() ignoring nested fields
LeCarbonator May 9, 2025
b8b1179
Merge branch 'fix-gh-1496' of github.com:LeCarbonator/tanstack-form i…
LeCarbonator May 9, 2025
3d14dee
chore: complete form-core unit test for FormLensApi
LeCarbonator May 9, 2025
1900857
feat: add react adapter to form lens api
LeCarbonator May 9, 2025
1eb15e9
fix: fix names for lens.Field and add test
LeCarbonator May 9, 2025
400abf9
chore: export WithFormLensProps
LeCarbonator May 9, 2025
8484d79
Merge branch 'main' into form-group-api
LeCarbonator May 9, 2025
dfe6f03
feat: add Subscribe and store to form lens
LeCarbonator May 10, 2025
019a61d
feat: add mount method to FormLensApi
LeCarbonator May 10, 2025
ea210c7
fix: memoize innerProps to avoid focus loss in withFormLens
LeCarbonator May 11, 2025
4f91b7f
refactor: use single useState instead of multiple useMemos
LeCarbonator May 11, 2025
2eb76fd
feat: allow nesting withFormLenses
LeCarbonator May 11, 2025
3710379
remove createFormGroup for redundancy
LeCarbonator May 11, 2025
e27d1e1
fix: widen typing of lens.Field/AppField to correct level
LeCarbonator May 11, 2025
be0b9d0
docs: add withFormLens section
LeCarbonator May 12, 2025
f749a95
fix: fix TName for lens component
LeCarbonator May 12, 2025
4024901
docs: fix typo in withFormLens
LeCarbonator May 12, 2025
e85a6b7
feat: add lensErrors to FormLensApi store
LeCarbonator May 12, 2025
635619b
chore: adjust memo dependency in useFormLens
LeCarbonator May 13, 2025
4ee6020
chore: call userEvent.setup() in createFormHook tests
LeCarbonator May 13, 2025
c9ed053
Merge branch 'main' into form-group-api
LeCarbonator May 13, 2025
3d4afea
refactor: move path concatenation to utils
LeCarbonator May 30, 2025
6b1e2e5
Merge branch 'main' of github.com:TanStack/form into form-group-api
LeCarbonator May 30, 2025
ca35b16
chore: move useLens to own file and rename
LeCarbonator May 30, 2025
6d6d24c
feat: add FieldsMap and createFieldMap utils
LeCarbonator May 31, 2025
ff55fa5
chore: migrate (most) lens references to field group
LeCarbonator May 31, 2025
4099ac5
chore: finalize migration from lens to field group
LeCarbonator May 31, 2025
69bec92
ci: apply automated fixes and generate docs
autofix-ci[bot] May 31, 2025
38e6db2
chore: remove accidental test file
LeCarbonator May 31, 2025
5b2c763
Merge branch 'form-group-api' of github.com:LeCarbonator/tanstack-for…
LeCarbonator May 31, 2025
69e313b
chore: add some unit tests for field mapping
LeCarbonator May 31, 2025
129cf17
chore: add unit tests
LeCarbonator Jun 1, 2025
15dca70
docs: update docs to use group
LeCarbonator Jun 1, 2025
59bc7cd
docs: add caveat with field mapping
LeCarbonator Jun 1, 2025
8da753b
docs: fix weird line break in alert text
LeCarbonator Jun 1, 2025
98aa36b
Merge branch 'main' into form-group-api
LeCarbonator Jun 2, 2025
8ce40fe
revert: remove FieldGroupApi#resetFieldMeta
LeCarbonator Jun 2, 2025
51a1942
refactor: allow null or undefined for field group keys
LeCarbonator Jun 6, 2025
4daaa6b
chore: add FieldGroupApi.test-d.ts
LeCarbonator Jun 6, 2025
02ce344
Merge branch 'form-group-api' of github.com:LeCarbonator/tanstack-for…
LeCarbonator Jul 3, 2025
20c163c
docs(react-form): amend large form example with withFieldGroup
LeCarbonator Jul 6, 2025
8a5e84a
chore(form-core): remove FieldGroupApi.reset
LeCarbonator Jul 8, 2025
afc83fe
Merge branch 'main' of github.com:TanStack/form into form-group-api
LeCarbonator Jul 12, 2025
c09f683
chore: fix broken unit test
LeCarbonator Jul 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions docs/framework/react/guides/form-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,162 @@ const ChildForm = withForm({
})
```

## Reusing groups of fields in multiple forms

Sometimes, a pair of fields are so closely related that it makes sense to group and reuse them — like the password example listed in the [linked fields guide](./linked-fields.md). Instead of repeating this logic across multiple forms, you can utilize the `withFormLens` higher-order component.

> Unlike `withForm`, validators cannot be specified and could be any value.
> Ensure that your fields can accept unknown error types.

Rewriting the passwords example using `withFormLens` would look like this:

```tsx
const { useAppForm, withForm, withFormLens } = createFormHook({
fieldComponents: {
TextField,
ErrorInfo,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})

type PasswordFields = {
password: string
confirm_password: string
}

// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
const defaultValues: PasswordFields = {
password: '',
confirm_password: '',
}

const PasswordFields = withFormLens({
defaultValues,
// You may also restrict the lens to only use forms that implement this submit meta.
// If none is provided, any form with the right defaultValues may use it.
// onSubmitMeta: { action: '' }

// Optional, but adds props to the `render` function in addition to `form`
props: {
// These default values are also for type-checking and are not used at runtime
title: 'Password',
},
// Internally, you will have access to a `lens` instead of a `form`
render: function Render({ lens, title }) {
// access reactive values using the lens store
const password = useStore(lens.store, (state) => state.values.password)
const isSubmitting = useStore(lens.store, (state) => state.isSubmitting)

return (
<div>
<h2>{title}</h2>
{/* Lenses also have access to Field, Subscribe, Field, AppField and AppForm */}
<lens.AppField name="password">
{(field) => <field.TextField label="Password" />}
</lens.AppField>
<lens.AppField
name="confirm_password"
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
// The form could be any values, so it is typed as 'unknown'
const values: unknown = fieldApi.form.state.values
// use the lens methods instead
if (value !== lens.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{(field) => (
<div>
<field.TextField label="Confirm Password" />
<field.ErrorInfo />
</div>
)}
</lens.AppField>
</div>
)
},
})
```

We can now use these grouped fields in any form that implements the default values:

```tsx
// You are allowed to extend the lens fields as long as the
// existing properties remain unchanged
type Account = PasswordFields & {
provider: string
username: string
}

// You may nest the lens fields wherever you want
type FormValues = {
name: string
age: number
account_data: PasswordFields
linked_accounts: Account[]
}

const defaultValues: FormValues = {
name: '',
age: 0,
account_data: {
password: '',
confirm_password: '',
},
linked_accounts: [
{
provider: 'TanStack',
username: '',
password: '',
confirm_password: '',
},
],
}

function App() {
const form = useAppForm({
defaultValues,
// If the lens didn't specify an `onSubmitMeta` property,
// the form may implement any meta it wants.
// Otherwise, the meta must be defined and match.
onSubmitMeta: { action: '' },
})

return (
<form.AppForm>
<PasswordFields
form={form}
// You must specify where the fields can be found
name="account_data"
title="Passwords"
/>
<form.Field name="linked_accounts" mode="array">
{(field) =>
field.state.value.map((account, i) => (
<PasswordFields
key={account.provider}
form={form}
// The fields may be in nested fields
name={`linked_accounts[${i}]`}
title={account.provider}
/>
))
}
</form.Field>
</form.AppForm>
)
}
```

## Tree-shaking form and field components

While the above examples are great for getting started, they're not ideal for certain use-cases where you might have hundreds of form and field components.
Expand Down
34 changes: 18 additions & 16 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,20 @@ export interface FormListeners<
}) => void
}

/**
* An object representing the base properties of a form, unrelated to any validators
*/
export interface BaseFormOptions<in out TFormData, in out TSubmitMeta = never> {
/**
* Set initial values for your form.
*/
defaultValues?: TFormData
/**
* onSubmitMeta, the data passed from the handleSubmit handler, to the onSubmit function props
*/
onSubmitMeta?: TSubmitMeta
}

/**
* An object representing the options for a form.
*/
Expand All @@ -329,11 +343,7 @@ export interface FormOptions<
in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
in out TSubmitMeta = never,
> {
/**
* Set initial values for your form.
*/
defaultValues?: TFormData
> extends BaseFormOptions<TFormData, TSubmitMeta> {
/**
* The default state for the form.
*/
Expand Down Expand Up @@ -376,11 +386,6 @@ export interface FormOptions<
TOnSubmitAsync
>

/**
* onSubmitMeta, the data passed from the handleSubmit handler, to the onSubmit function props
*/
onSubmitMeta?: TSubmitMeta

/**
* form level listeners
*/
Expand Down Expand Up @@ -2136,12 +2141,9 @@ export class FormApi<
...prev.fieldMetaBase,
[field]: defaultFieldMeta,
},
values: {
...prev.values,
[field]:
this.options.defaultValues &&
this.options.defaultValues[field as keyof TFormData],
},
values: this.options.defaultValues
? setBy(prev.values, field, getBy(this.options.defaultValues, field))
: prev.values,
}
})
}
Expand Down
Loading