Skip to content

feat(react-form): Add withFieldGroup #1469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
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
232 changes: 232 additions & 0 deletions docs/framework/react/guides/form-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,238 @@ 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 `withFieldGroup` 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 `withFieldGroup` would look like this:

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

type PasswordFields = {
password: string
confirm_password: string
}

// These default values are not used at runtime, but the keys are needed for mapping purposes.
// This allows you to spread `formOptions` without needing to redeclare it.
const defaultValues: PasswordFields = {
password: '',
confirm_password: '',
}

const FieldGroupPasswordField = withFieldGroup({
defaultValues,
// You may also restrict the group 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 `group` instead of a `form`
render: function Render({ group, title }) {
// access reactive values using the group store
const password = useStore(group.store, (state) => state.values.password)
// or the form itself
const isSubmitting = useStore(
group.form.store,
(state) => state.isSubmitting,
)

return (
<div>
<h2>{title}</h2>
{/* Groups also have access to Field, Subscribe, Field, AppField and AppForm */}
<group.AppField name="password">
{(field) => <field.TextField label="Password" />}
</group.AppField>
<group.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 group methods instead
if (value !== group.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{(field) => (
<div>
<field.TextField label="Confirm Password" />
<field.ErrorInfo />
</div>
)}
</group.AppField>
</div>
)
},
})
```

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

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

// You may nest the group 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 group 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
fields="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
fields={`linked_accounts[${i}]`}
title={account.provider}
/>
))
}
</form.Field>
</form.AppForm>
)
}
```

### Mapping field group values to a different field

You may want to keep the password fields on the top level of your form, or rename the properties for clarity. You can map field group values
to their true location by changing the `field` property:

> [!IMPORTANT]
> Due to TypeScript limitations, field mapping is only allowed for objects. You can use records or arrays at the top level of a field group, but you will not be able to map the fields.

```tsx
// To have an easier form, you can keep the fields on the top level
type FormValues = {
name: string
age: number
password: string
confirm_password: string
}

const defaultValues: FormValues = {
name: '',
age: 0,
password: '',
confirm_password: '',
}

function App() {
const form = useAppForm({
defaultValues,
})

return (
<form.AppForm>
<PasswordFields
form={form}
// You can map the fields to their equivalent deep key
fields={{
password: 'password',
confirm_password: 'confirm_password',
// or map them to differently named keys entirely
// 'password': 'name'
}}
title="Passwords"
/>
</form.AppForm>
)
}
```

If you expect your fields to always be at the top level of your form, you can create a quick map
of your field groups using a helper function:

```tsx
const defaultValues: PasswordFields = {
password: '',
confirm_password: '',
}

const passwordFields = createFieldMap(defaultValues)
/* This generates the following map:
{
'password': 'password',
'confirm_password': 'confirm_password'
}
*/

// Usage:
<PasswordFields
form={form}
fields={passwordFields}
title="Passwords"
/>
```

## 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
Loading