Skip to content

Commit 4db35b6

Browse files
committed
Improve documentation
1 parent 15b6447 commit 4db35b6

File tree

5 files changed

+176
-102
lines changed

5 files changed

+176
-102
lines changed

docs/ReferenceInput.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ You can make the `getList()` call lazy by using the `enableGetChoices` prop. Thi
154154
<ReferenceInput
155155
source="company_id"
156156
reference="companies"
157-
enableGetChoices={({ q }) => !!(q && q.length >= 2)}
157+
enableGetChoices={({ q }) => q && q.length >= 2}
158158
/>
159159
```
160160

docs_headless/src/content/docs/ReferenceArrayInputBase.md

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const PostEdit = () => (
3939
);
4040

4141
const TagSelector = () => {
42-
const { choices, isLoading, error } = useChoicesContext();
43-
const { field, id } = useInput();
42+
const { choices, isLoading, error, source } = useChoicesContext();
43+
const { field, id } = useInput({ source });
4444

4545
if (isLoading) return <div>Loading...</div>;
4646
if (error) return <div>Error: {error.message}</div>;
@@ -97,7 +97,7 @@ dataProvider.getList('tags', {
9797
});
9898
```
9999

100-
`<ReferenceArrayInputBase>` handles the data fetching and provides the choices through a [`ChoicesContext`](./useChoicesContext.md). It's up to the child components to render the selection interface.
100+
`<ReferenceArrayInputBase>` handles the data fetching and provides the choices through a [`ChoicesContext`](./usechoicescontext). It's up to the child components to render the selection interface.
101101

102102
You can tweak how `<ReferenceArrayInputBase>` fetches the possible values using the `page`, `perPage`, `sort`, and `filter` props.
103103

@@ -107,8 +107,7 @@ You can tweak how `<ReferenceArrayInputBase>` fetches the possible values using
107107
|--------------------|----------|---------------------------------------------|------------------------------------|---------------------------------------------------------------------------------------------------------------------|
108108
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
109109
| `reference` | Required | `string` | '' | Name of the reference resource, e.g. 'tags'. |
110-
| `children` | Optional | `ReactNode` | - | The actual selection component |
111-
| `render` | Optional | `(context) => ReactNode` | - | Function that takes the choices context and renders the selection interface |
110+
| `children` | Required | `ReactNode` | - | The actual selection component |
112111
| `enableGetChoices` | Optional | `({q: string}) => boolean` | `() => true` | Function taking the `filterValues` and returning a boolean to enable the `getList` call. |
113112
| `filter` | Optional | `Object` | `{}` | Permanent filters to use for getting the suggestion list |
114113
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
@@ -126,8 +125,8 @@ You can access the choices context using the `useChoicesContext` hook.
126125
import { ReferenceArrayInputBase, useChoicesContext, useInput } from 'ra-core';
127126

128127
export const CustomArraySelector = () => {
129-
const { choices, isLoading, error } = useChoicesContext();
130-
const { field, id } = useInput();
128+
const { choices, isLoading, error, source } = useChoicesContext();
129+
const { field, id } = useInput({ source });
131130

132131
if (isLoading) {
133132
return <div>Loading...</div>;
@@ -171,44 +170,6 @@ export const MyReferenceArrayInput = () => (
171170
);
172171
```
173172

174-
## `render`
175-
176-
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ChoicesContext` as argument.
177-
178-
```jsx
179-
export const MyReferenceArrayInput = () => (
180-
<ReferenceArrayInputBase
181-
source="tag_ids"
182-
reference="tags"
183-
render={({ choices, isLoading, error }) => {
184-
if (isLoading) {
185-
return <div>Loading...</div>;
186-
}
187-
188-
if (error) {
189-
return (
190-
<div className="error">
191-
{error.message}
192-
</div>
193-
);
194-
}
195-
196-
return (
197-
<select multiple>
198-
{choices.map(choice => (
199-
<option key={choice.id} value={choice.id}>
200-
{choice.name}
201-
</option>
202-
))}
203-
</select>
204-
);
205-
}}
206-
/>
207-
);
208-
```
209-
210-
The `render` function prop will take priority on `children` props if both are set.
211-
212173
## `enableGetChoices`
213174

214175
You can make the `getList()` call lazy by using the `enableGetChoices` prop. This prop should be a function that receives the `filterValues` as parameter and return a boolean. This can be useful when using a search input on a resource with a lot of data. The following example only starts fetching the options when the query has at least 2 characters:

docs_headless/src/content/docs/ReferenceInputBase.md

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ const ContactEdit = () => (
4141
);
4242

4343
const CompanySelector = () => {
44-
const { choices, isLoading, error } = useChoicesContext();
45-
const { field, id } = useInput();
44+
const { allChoices, isLoading, error, source } = useChoicesContext();
45+
const { field, id } = useInput({ source });
4646

4747
if (isLoading) return <div>Loading...</div>;
4848
if (error) return <div>Error: {error.message}</div>;
@@ -52,7 +52,7 @@ const CompanySelector = () => {
5252
<label htmlFor={id}>Company</label>
5353
<select id={id} {...field}>
5454
<option value="">Select a company</option>
55-
{choices.map(choice => (
55+
{allChoices.map(choice => (
5656
<option key={choice.id} value={choice.id}>
5757
{choice.name}
5858
</option>
@@ -87,7 +87,7 @@ dataProvider.getList('companies', {
8787
});
8888
```
8989

90-
`<ReferenceInputBase>` handles the data fetching and provides the choices through a [`ChoicesContext`](./useChoicesContext.md). It's up to the child components to render the selection interface.
90+
`<ReferenceInputBase>` handles the data fetching and provides the choices through a [`ChoicesContext`](./usechoicescontext). It's up to the child components to render the selection interface.
9191

9292
You can tweak how `<ReferenceInputBase>` fetches the possible values using the `page`, `perPage`, `sort`, and `filter` props.
9393

@@ -97,8 +97,7 @@ You can tweak how `<ReferenceInputBase>` fetches the possible values using the `
9797
|--------------------|----------|---------------------------------------------|----------------------------------|------------------------------------------------------------------------------------------------|
9898
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
9999
| `reference` | Required | `string` | '' | Name of the reference resource, e.g. 'companies'. |
100-
| `children` | Optional | `ReactNode` | - | The actual selection component |
101-
| `render` | Optional | `(context) => ReactNode` | - | Function that takes the choices context and renders the selection interface |
100+
| `children` | Required | `ReactNode` | - | The actual selection component |
102101
| `enableGetChoices` | Optional | `({q: string}) => boolean` | `() => true` | Function taking the `filterValues` and returning a boolean to enable the `getList` call. |
103102
| `filter` | Optional | `Object` | `{}` | Permanent filters to use for getting the suggestion list |
104103
| `page` | Optional | `number` | 1 | The current page number |
@@ -116,12 +115,8 @@ You can access the choices context using the `useChoicesContext` hook.
116115
import { ReferenceInputBase, useChoicesContext, useInput } from 'ra-core';
117116

118117
export const CustomSelector = () => {
119-
const { choices, isLoading, error } = useChoicesContext();
120-
const { field, id } = useInput();
121-
122-
if (isLoading) {
123-
return <div>Loading...</div>;
124-
}
118+
const { allChoices, isLoading, error, source } = useChoicesContext();
119+
const { field, id } = useInput({ source });
125120

126121
if (error) {
127122
return <div className="error">{error.toString()}</div>;
@@ -131,8 +126,9 @@ export const CustomSelector = () => {
131126
<div>
132127
<label htmlFor={id}>Company</label>
133128
<select id={id} {...field}>
129+
{isPending && <option value="">Loading...</option>}
134130
<option value="">Select a company</option>
135-
{choices.map(choice => (
131+
{allChoices.map(choice => (
136132
<option key={choice.id} value={choice.id}>
137133
{choice.name}
138134
</option>
@@ -149,45 +145,6 @@ export const MyReferenceInput = () => (
149145
);
150146
```
151147

152-
## `render`
153-
154-
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ChoicesContext` as argument.
155-
156-
```jsx
157-
export const MyReferenceInput = () => (
158-
<ReferenceInputBase
159-
source="company_id"
160-
reference="companies"
161-
render={({ choices, isLoading, error }) => {
162-
if (isLoading) {
163-
return <div>Loading...</div>;
164-
}
165-
166-
if (error) {
167-
return (
168-
<div className="error">
169-
{error.message}
170-
</div>
171-
);
172-
}
173-
174-
return (
175-
<select>
176-
<option value="">Select a company</option>
177-
{choices.map(choice => (
178-
<option key={choice.id} value={choice.id}>
179-
{choice.name}
180-
</option>
181-
))}
182-
</select>
183-
);
184-
}}
185-
/>
186-
);
187-
```
188-
189-
The `render` function prop will take priority on `children` props if both are set.
190-
191148
## `enableGetChoices`
192149

193150
You can make the `getList()` call lazy by using the `enableGetChoices` prop. This prop should be a function that receives the `filterValues` as parameter and return a boolean. This can be useful when using a search input on a resource with a lot of data. The following example only starts fetching the options when the query has at least 2 characters:
@@ -196,7 +153,7 @@ You can make the `getList()` call lazy by using the `enableGetChoices` prop. Thi
196153
<ReferenceInputBase
197154
source="company_id"
198155
reference="companies"
199-
enableGetChoices={({ q }) => !!(q && q.length >= 2)}
156+
enableGetChoices={({ q }) => q && q.length >= 2}
200157
/>
201158
```
202159

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: "useChoicesContext"
3+
---
4+
5+
The [`<ReferenceInputBase>`](./referenceinputbase) and [`<ReferenceArrayInputBase>`](./referencearrayinputbase) components create a `ChoicesContext` to store the choices, as well as filters, pagination, sort state, and callbacks to update them.
6+
7+
The `ChoicesContext` is very similar to the [`ListContext`](./uselistcontext) with the exception that it does not return a `data` property but 3 choices related properties:
8+
9+
- `availableChoices`: The choices that are not selected but match the parameters (sorting, pagination and filters)
10+
- `selectedChoices`: The selected choices.
11+
- `allChoices`: Merge of both available and selected choices.
12+
13+
## Usage
14+
15+
Call `useChoicesContext` in a component, then use this component as a descendant of a `ReferenceInput` or `ReferenceArrayInput` component.
16+
17+
```jsx
18+
// in src/comments/CompanySelector.tsx
19+
import { useChoicesContext, useInput } from 'ra-core';
20+
21+
export const CompanySelector = () => {
22+
const { allChoices, isLoading, error, source } = useChoicesContext();
23+
const { field, id } = useInput({ source });
24+
25+
if (isLoading) return <div>Loading...</div>;
26+
if (error) return <div>Error: {error.message}</div>;
27+
28+
return (
29+
<div>
30+
<label htmlFor={id}>Company</label>
31+
<select id={id} {...field}>
32+
<option value="">Select a company</option>
33+
{allChoices.map(choice => (
34+
<option key={choice.id} value={choice.id}>
35+
{choice.name}
36+
</option>
37+
))}
38+
</select>
39+
</div>
40+
);
41+
};
42+
43+
// in src/comments/CommentCreate.js
44+
import { CreateBase, ReferenceInputBase, Form } from 'ra-core';
45+
import { PostInput } from './PostInput';
46+
47+
export const EmployeeCreate = () => (
48+
<CreateBase>
49+
<Form>
50+
<ReferenceInputBase source="company_id" reference="companies">
51+
<CompanySelector />
52+
</ReferenceInputBase>
53+
<TextInput source="body" />
54+
</Form>
55+
</CreateBase>
56+
)
57+
```
58+
59+
## Return Value
60+
61+
The `useChoicesContext` hook returns an object with the following keys:
62+
63+
```jsx
64+
const {
65+
// fetched data
66+
allChoices, // an array of the choices records, e.g. [{ id: 123, title: 'hello world' }, { ... }], both available and selected.
67+
availableChoices, // an array of the available choices records, e.g. [{ id: 123, title: 'hello world' }, { ... }],.
68+
selectedChoices, // an array of the selected choices records, e.g. [{ id: 123, title: 'hello world' }, { ... }],.
69+
total, // the total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
70+
isFetching, // boolean that is true while the data is being fetched, and false once the data is fetched
71+
isLoading, // boolean that is true until the data has been fetched for the first time
72+
isPending, // boolean that is true until the data is available for the first time
73+
error, // Will contain any error that occurred while fetching data
74+
// pagination
75+
page, // the current page. Starts at 1
76+
perPage, // the number of results per page. Defaults to 25
77+
setPage, // a callback to change the page, e.g. setPage(3)
78+
setPerPage, // a callback to change the number of results per page, e.g. setPerPage(25)
79+
hasPreviousPage, // boolean, true if the current page is not the first one
80+
hasNextPage, // boolean, true if the current page is not the last one
81+
// sorting
82+
sort, // a sort object { field, order }, e.g. { field: 'date', order: 'DESC' }
83+
setSort, // a callback to change the sort, e.g. setSort({ field: 'name', order: 'ASC' })
84+
// filtering
85+
filter, // The permanent filter values, e.g. { title: 'lorem', nationality: 'fr' }
86+
filterValues, // a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' }
87+
displayedFilters, // a dictionary of the displayed filters, e.g. { title: true, nationality: true }
88+
setFilters, // a callback to update the filters, e.g. setFilters(filters, displayedFilters)
89+
showFilter, // a callback to show one of the filters, e.g. showFilter('title', defaultValue)
90+
hideFilter, // a callback to hide one of the filters, e.g. hideFilter('title')
91+
// misc
92+
resource, // the resource name, deduced from the location. e.g. 'posts'
93+
refetch, // callback for fetching the list data again
94+
source, // the name of the field containing the currently selected record(s).
95+
} = useChoicesContext();
96+
```

packages/ra-core/src/controller/input/ReferenceInputBase.stories.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,15 @@ const SelectInput = (
138138
) => {
139139
const { allChoices, error, isPending, source } = useChoicesContext(props);
140140
const { getChoiceValue, getChoiceText } = useChoices(props);
141-
const { field } = useInput({ ...props, source });
141+
const { field, id } = useInput({ ...props, source });
142142

143143
if (error) {
144144
return <div style={{ color: 'red' }}>{error.message}</div>;
145145
}
146146
return (
147147
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
148-
<label htmlFor={field.name}>{props.label || field.name}</label>
149-
<select id={field.name} {...field}>
148+
<label htmlFor={id}>{props.label || field.name}</label>
149+
<select id={id} {...field}>
150150
{isPending && <option value="">Loading...</option>}
151151
{allChoices?.map(choice => (
152152
<option
@@ -574,3 +574,63 @@ const RenderChildOnDemand = ({ children }) => {
574574
</>
575575
);
576576
};
577+
578+
export const FullHeadlessStory = () => {
579+
return (
580+
<TestMemoryRouter initialEntries={['/books/1']}>
581+
<CoreAdmin dataProvider={dataProviderWithAuthors}>
582+
<Resource
583+
name="books"
584+
edit={
585+
<EditBase
586+
mutationMode="pessimistic"
587+
mutationOptions={{
588+
onSuccess: data => {
589+
console.log(data);
590+
},
591+
}}
592+
>
593+
<Form>
594+
<ReferenceInputBase
595+
reference="authors"
596+
source="author"
597+
>
598+
<CustomSelector />
599+
</ReferenceInputBase>
600+
<button type="submit">Save</button>
601+
</Form>
602+
</EditBase>
603+
}
604+
/>
605+
</CoreAdmin>
606+
</TestMemoryRouter>
607+
);
608+
};
609+
610+
const CustomSelector = (
611+
props: Omit<InputProps, 'source'> &
612+
Partial<Pick<InputProps, 'source'>> &
613+
ChoicesProps & { source?: string }
614+
) => {
615+
const { allChoices, isPending, error, source } = useChoicesContext(props);
616+
const { field, id } = useInput({ ...props, source });
617+
618+
if (error) {
619+
return <div className="error">{error.message}</div>;
620+
}
621+
622+
return (
623+
<div>
624+
<label htmlFor={id}>Author</label>
625+
<select id={id} {...field}>
626+
{isPending && <option value="">Loading...</option>}
627+
<option value="">Select an author</option>
628+
{allChoices.map(choice => (
629+
<option key={choice.id} value={choice.id}>
630+
{choice.first_name} {choice.last_name}
631+
</option>
632+
))}
633+
</select>
634+
</div>
635+
);
636+
};

0 commit comments

Comments
 (0)