Skip to content

Commit b5e262f

Browse files
authored
Merge pull request #10991 from marmelab/doc/form-data-consumer
[Doc] Add <FormDataConsumer> and useSourceContext headless documentation
2 parents cc477d8 + b6075a5 commit b5e262f

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed

docs_headless/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,10 @@ export default defineConfig({
183183
'referencemanyinputbase',
184184
'referencemanytomanyinputbase',
185185
'referenceoneinputbase',
186+
'formdataconsumer',
186187
'usechoicescontext',
187188
'useinput',
189+
'usesourcecontext',
188190
],
189191
},
190192
{
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
title: <FormDataConsumer>
3+
---
4+
5+
Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former).
6+
7+
The `<FormDataConsumer>` component gets the current (edited) values of the record and passes it to a child function.
8+
9+
## Usage
10+
11+
As `<FormDataConsumer>` uses the render props pattern, you can avoid creating an intermediate component like the `<CityInput>` component above:
12+
13+
```tsx
14+
import * as React from 'react';
15+
import { Edit, SimpleForm, SelectInput, FormDataConsumer } from 'react-admin';
16+
17+
const countries = ['USA', 'UK', 'France'];
18+
const cities: Record<string, string[]> = {
19+
USA: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
20+
UK: ['London', 'Birmingham', 'Glasgow', 'Liverpool', 'Bristol'],
21+
France: ['Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice'],
22+
};
23+
const toChoices = (items: string[]) =>
24+
items.map(item => ({ id: item, name: item }));
25+
26+
const OrderEdit = () => (
27+
<Edit>
28+
<SimpleForm>
29+
<SelectInput source="country" choices={toChoices(countries)} />
30+
<FormDataConsumer<{ country: string }>>
31+
{({ formData, ...rest }) => (
32+
<SelectInput
33+
source="cities"
34+
choices={
35+
formData.country
36+
? toChoices(cities[formData.country])
37+
: []
38+
}
39+
{...rest}
40+
/>
41+
)}
42+
</FormDataConsumer>
43+
</SimpleForm>
44+
</Edit>
45+
);
46+
```
47+
48+
## Hiding Inputs Based On Other Inputs
49+
50+
You may want to display or hide inputs based on the value of another input - for instance, show an `email` input only if the `hasEmail` boolean input has been ticked to true.
51+
52+
For such cases, you can use the approach described above, using the `<FormDataConsumer>` component.
53+
54+
```tsx
55+
import { FormDataConsumer } from 'react-admin';
56+
57+
const PostEdit = () => (
58+
<Edit>
59+
<SimpleForm shouldUnregister>
60+
<BooleanInput source="hasEmail" />
61+
<FormDataConsumer<{ hasEmail: boolean }>>
62+
{({ formData, ...rest }) =>
63+
formData.hasEmail && <TextInput source="email" {...rest} />
64+
}
65+
</FormDataConsumer>
66+
</SimpleForm>
67+
</Edit>
68+
);
69+
```
70+
71+
:::note
72+
By default, `react-hook-form` submits values of unmounted input components. In the above example, the `shouldUnregister` prop of the `<SimpleForm>` component prevents that from happening. That way, when end users hide an input, its value isn’t included in the submitted data.
73+
:::
74+
75+
:::note
76+
`shouldUnregister` should be avoided when using `<ArrayInput>` (which internally uses `useFieldArray`) as the unregister function gets called after input unmount/remount and reorder. This limitation is mentioned in the `react-hook-form` [documentation](https://react-hook-form.com/docs/usecontroller#props). If you are in such a situation, you can use the [`transform`](./EditBase.md#transform) prop to manually clean the submitted values.
77+
:::
78+
79+
## Usage inside an ArrayInput
80+
81+
When used inside an `<ArrayInput>`, `<FormDataConsumer>` provides one additional property to its child function called scopedFormData. It’s an object containing the current values of the currently rendered item. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
82+
83+
```tsx
84+
import { FormDataConsumer } from 'react-admin';
85+
86+
const PostEdit = () => (
87+
<Edit>
88+
<SimpleForm>
89+
<ArrayInput source="authors">
90+
<SimpleFormIterator>
91+
<TextInput source="name" />
92+
<FormDataConsumer<{ name: string }>>
93+
{({
94+
formData, // The whole form data
95+
scopedFormData, // The data for this item of the ArrayInput
96+
...rest
97+
}) =>
98+
scopedFormData && scopedFormData.name ? (
99+
<SelectInput
100+
source="role" // Will translate to "authors[0].role"
101+
choices={[
102+
{ id: 1, name: 'Head Writer' },
103+
{ id: 2, name: 'Co-Writer' },
104+
]}
105+
{...rest}
106+
/>
107+
) : null
108+
}
109+
</FormDataConsumer>
110+
</SimpleFormIterator>
111+
</ArrayInput>
112+
</SimpleForm>
113+
</Edit>
114+
);
115+
```
116+
117+
:::tip
118+
TypeScript users will notice that scopedFormData is typed as an optional parameter. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, this parameter will be undefined. If you are inside an `<ArrayInput>`, you can safely assume that this parameter will be defined.
119+
:::
120+
121+
:::tip
122+
If you need to access the effective source of an input inside an `<ArrayInput>`, for example to change the value programmatically using setValue, you will need to leverage the [`useSourceContext`](./useSourceContext.md) hook.
123+
:::
124+
125+
## Props
126+
127+
| Prop | Required | Type | Default | Description |
128+
| ---------- | -------- | ---------- | ------- | -------------------------------------------------------------- |
129+
| `children` | Required | `function` | - | A function that takes the `formData` and returns a `ReactNode` |
130+
131+
## `children`
132+
133+
The function used to render a component based on the `formData`.
134+
135+
```tsx
136+
<FormDataConsumer<{ name: string }>>
137+
{({
138+
formData, // The whole form data
139+
scopedFormData, // The data for this item of the ArrayInput
140+
}) => {
141+
/* ... */
142+
}}
143+
</FormDataConsumer>
144+
```
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
title: useSourceContext
3+
---
4+
5+
When using an `<ArrayInput>`, the `name` under which an input is registered in a `<Form>` is dynamically generated depending on the index of the item in the array.
6+
7+
To get the `name` of the input for a given index, you can leverage the `SourceContext` created by `ra-core`, which can be accessed using the `useSourceContext` hook.
8+
9+
You can then leverage `react-hook-form`’s <a href="https://react-hook-form.com/docs/useform/setvalue" target="_blank" rel="noreferrer">`setValue`</a> method to change an item’s value programmatically.
10+
11+
```tsx
12+
import { useSourceContext } from 'ra-core';
13+
import { useFormContext } from 'react-hook-form';
14+
import {
15+
ArrayInput,
16+
Button,
17+
SimpleFormIterator,
18+
TextInput,
19+
} from 'your-ra-ui-library';
20+
21+
const MakeAdminButton = () => {
22+
const sourceContext = useSourceContext();
23+
const { setValue } = useFormContext();
24+
25+
const onClick = () => {
26+
// sourceContext.getSource('role') will for instance return
27+
// 'users.0.role'
28+
setValue(sourceContext.getSource('role'), 'admin');
29+
};
30+
31+
return (
32+
<Button onClick={onClick} size="small" sx={{ minWidth: 120 }}>
33+
Make admin
34+
</Button>
35+
);
36+
};
37+
38+
const UserArray = () => (
39+
<ArrayInput source="users">
40+
<SimpleFormIterator inline>
41+
<TextInput source="name" helperText={false} />
42+
<TextInput source="role" helperText={false} />
43+
<MakeAdminButton />
44+
</SimpleFormIterator>
45+
</ArrayInput>
46+
);
47+
```
48+
49+
## Hook Value
50+
51+
| Name | Type | Description |
52+
| ----------- | ---------- | ----------------------------------------------------------------------- |
53+
| `getSource` | `function` | A function that returns the `name` of the input for the given `source` |
54+
| `getLabel` | `function` | A function that returns the `label` of the input for the given `source` |
55+
56+
## `getSource`
57+
58+
The `getSource` function returns the `name` of a `source` withing a `SourceContext`.
59+
60+
```tsx
61+
export function MyCustomInput({ source }: MyCustomInputProps) {
62+
const sourceContext = useSourceContext();
63+
const name = sourceContext.getSource(source);
64+
65+
return /* ... */;
66+
}
67+
68+
export type MyCustomInputProps = {
69+
source: string;
70+
};
71+
```
72+
73+
## `getLabel`
74+
75+
The `getLabel` function returns the `label` of a `source` withing a `SourceContext`.
76+
77+
```tsx
78+
export function MyCustomInput({ source }: MyCustomInputProps) {
79+
const sourceContext = useSourceContext();
80+
const label = sourceContext.getLabel(source);
81+
82+
return /* ... */;
83+
}
84+
85+
export type MyCustomInputProps = {
86+
source: string;
87+
};
88+
```

docs_headless/src/styles/global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,9 @@ a[aria-current='page'].enterprise span:not(.sl-badge)::after {
150150
background-image: url('/public/premium-light.svg');
151151
}
152152
}
153+
154+
table {
155+
display: table;
156+
min-width: 100%;
157+
table-layout: auto;
158+
}

packages/ra-core/src/form/FormDataConsumer.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,32 @@ export type FormDataConsumerRender<
101101

102102
interface ConnectedProps<TFieldValues extends FieldValues = FieldValues> {
103103
children: FormDataConsumerRender<TFieldValues>;
104+
/**
105+
* @deprecated This prop will be removed in a future major release.
106+
*/
104107
form?: string;
108+
109+
/**
110+
* @deprecated This prop will be removed in a future major release.
111+
*/
105112
record?: any;
113+
114+
/**
115+
* @deprecated This prop will be removed in a future major release.
116+
*/
106117
source?: string;
118+
119+
/**
120+
* @deprecated This prop will be removed in a future major release.
121+
*/
107122
[key: string]: any;
108123
}
109124

110125
interface Props<TFieldValues extends FieldValues> extends ConnectedProps {
111126
formData: TFieldValues;
127+
128+
/**
129+
* @deprecated This prop will be removed in a future major release.
130+
*/
112131
index?: number;
113132
}

0 commit comments

Comments
 (0)