Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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/solid/guides/form-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,238 @@ function App() {

While hooks are the future of Solid, higher-order components are still a powerful tool for composition. In particular, the API of `withForm` enables us to have strong type-safety without requiring users to pass generics.

## 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 FieldGroupPasswordFields = 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>
<FieldGroupPasswordFields
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) => (
<FieldGroupPasswordFields
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>
<FieldGroupPasswordFields
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:
<FieldGroupPasswordFields
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
119 changes: 119 additions & 0 deletions docs/framework/solid/reference/functions/createformhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,122 @@ withForm: <TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync
##### Returns

`Element`


### withFieldGroup()

```ts
withFieldGroup: <TFieldGroupData, TSubmitMeta, TRenderProps>(__namedParameters) => <TFormData, TFields, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TFormSubmitMeta>(params) => Element;
```

#### Type Parameters

• **TFieldGroupData**

• **TSubmitMeta**

• **TRenderProps** *extends* `Record`\<`string`, `unknown`\> = \{\}

#### Parameters

##### \_\_namedParameters

[`WithFieldGroupProps`](../../interfaces/withfieldgroupprops.md)\<`TFieldGroupData`, `TComponents`, `TFormComponents`, `TSubmitMeta`, `TRenderProps`\>

#### Returns

`Function`

##### Type Parameters

• **TFormData**

• **TFields** *extends*
\| `string`
\| \{ \[K in string \| number \| symbol\]: DeepKeysOfType\<TFormData, TFieldGroupData\[K\]\> \}

• **TOnMount** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnChange** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnChangeAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnBlur** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnBlurAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnSubmit** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TFormSubmitMeta**

##### Parameters

###### params

`PropsWithChildren`\<`NoInfer`\<`TRenderProps`\> & `object`\>

##### Returns

`Element`

### withForm()

```ts
withForm: <TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, TRenderProps>(__namedParameters) => (props) => Element;
```

#### Type Parameters

• **TFormData**

• **TOnMount** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnChange** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnChangeAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnBlur** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnBlurAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnSubmit** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnSubmitAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnDynamic** *extends* `undefined` \| `FormValidateOrFn`\<`TFormData`\>

• **TOnDynamicAsync** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TOnServer** *extends* `undefined` \| `FormAsyncValidateOrFn`\<`TFormData`\>

• **TSubmitMeta**

• **TRenderProps** *extends* `object` = \{\}

#### Parameters

##### \_\_namedParameters

[`WithFormProps`](../../interfaces/withformprops.md)\<`TFormData`, `TOnMount`, `TOnChange`, `TOnChangeAsync`, `TOnBlur`, `TOnBlurAsync`, `TOnSubmit`, `TOnSubmitAsync`, `TOnDynamic`, `TOnDynamicAsync`, `TOnServer`, `TSubmitMeta`, `TComponents`, `TFormComponents`, `TRenderProps`\>

#### Returns

`Function`

##### Parameters

###### props

`PropsWithChildren`\<`NoInfer`\<`UnwrapOrAny`\<`TRenderProps`\>\> & `object`\>

##### Returns

`Element`
Loading