Skip to content

Commit 54bb7e1

Browse files
[Doc] Add <FormDataConsumer> and useSourceContext headless documentation
1 parent b425e51 commit 54bb7e1

File tree

5 files changed

+269
-4
lines changed

5 files changed

+269
-4
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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
@todo: add useSourceContext Page
126+
127+
## Props
128+
129+
| Prop | Required | Type | Default | Description |
130+
| ---------- | -------- | ---------- | ------- | ----------------------------------------------------------------- |
131+
| `children` | Required | `function` | - | A function that takes the `formData` and returns a `ReactElement` |
132+
133+
## `children`
134+
135+
The function used to render a component based on the `formData`.
136+
137+
```tsx
138+
<FormDataConsumer<{ name: string }>>
139+
{({
140+
formData, // The whole form data
141+
scopedFormData, // The data for this item of the ArrayInput
142+
}) => {
143+
/* ... */
144+
}}
145+
</FormDataConsumer>
146+
```
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: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,21 @@ a[aria-current='page'].enterprise span:not(.sl-badge)::after {
128128
background-image: url('/public/premium-dark.svg');
129129
}
130130

131-
[data-theme="light"] a.enterprise span:not(.sl-badge)::after {
131+
[data-theme='light'] a.enterprise span:not(.sl-badge)::after {
132132
background-image: url('/public/premium-light.svg');
133133
}
134-
[data-theme="light"] a[aria-current='page'].enterprise span:not(.sl-badge)::after {
134+
[data-theme='light']
135+
a[aria-current='page'].enterprise
136+
span:not(.sl-badge)::after {
135137
background-image: url('/public/premium-dark.svg');
136138
}
137139

138-
[data-theme="dark"] a.enterprise span:not(.sl-badge)::after {
140+
[data-theme='dark'] a.enterprise span:not(.sl-badge)::after {
139141
background-image: url('/public/premium-dark.svg');
140142
}
141-
[data-theme="dark"] a[aria-current='page'].enterprise span:not(.sl-badge)::after {
143+
[data-theme='dark']
144+
a[aria-current='page'].enterprise
145+
span:not(.sl-badge)::after {
142146
background-image: url('/public/premium-light.svg');
143147
}
144148

@@ -150,3 +154,9 @@ a[aria-current='page'].enterprise span:not(.sl-badge)::after {
150154
background-image: url('/public/premium-light.svg');
151155
}
152156
}
157+
158+
table {
159+
display: table;
160+
min-width: 100%;
161+
table-layout: auto;
162+
}

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)