Skip to content

Commit 2ba1258

Browse files
authored
Merge pull request #10344 from marmelab/AutosubmitForm
Introduce `<FilterLiveForm>`
2 parents 3eb11c3 + 7fd23c6 commit 2ba1258

27 files changed

Lines changed: 1906 additions & 381 deletions

docs/FilterList.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,96 @@ The children of `<FilterList>` must be a list of `<FilterListItem>` components.
279279
| `isSelected` | Optional | function | | A function that receives the item value and the currently applied filters. It must return a boolean. |
280280
| `toggleFilter` | Optional | function | | A function that receives the item value and the currently applied filters. It is called when user toggles a filter and must return the new filters to apply. |
281281

282+
## Using Inputs
283+
284+
If you want to add a simple text input to the sidebar, you can use the [`<FilterLiveSearch>`](./FilterLiveSearch.md) component alongside `<FilterList>` in the `<List>` sidebar. It will render a simple text input, which will filter the list based on the value entered by the user.
285+
286+
{% raw %}
287+
```jsx
288+
import { FilterLiveSearch, FilterList, FilterListItem } from 'react-admin';
289+
import { Card, CardContent } from '@mui/material';
290+
import MailIcon from '@mui/icons-material/MailOutline';
291+
292+
export const PostFilterSidebar = () => (
293+
<Card sx={{ order: -1, mr: 2, mt: 6, width: 250, height: 'fit-content' }}>
294+
<CardContent>
295+
<FilterLiveSearch source="q" label="Search" />
296+
<FilterList label="Subscribed to newsletter" icon={<MailIcon />}>
297+
<FilterListItem label="Yes" value={{ has_newsletter: true }} />
298+
<FilterListItem label="No" value={{ has_newsletter: false }} />
299+
</FilterList>
300+
</CardContent>
301+
</Card>
302+
);
303+
```
304+
{% endraw %}
305+
306+
If you want to use other type of inputs, such as a `<ReferenceInput>`, you can use the [`<FilterLiveForm>`](./FilterLiveForm.md) component to create a form that automatically updates the filters when the user changes the value of an input.
307+
308+
{% raw %}
309+
```tsx
310+
import * as React from 'react';
311+
import CategoryIcon from '@mui/icons-material/LocalOffer';
312+
import Person2Icon from '@mui/icons-material/Person2';
313+
import TitleIcon from '@mui/icons-material/Title';
314+
import { Card, CardContent } from '@mui/material';
315+
import {
316+
AutocompleteInput,
317+
FilterLiveForm,
318+
Datagrid,
319+
FilterList,
320+
FilterListItem,
321+
FilterListSection,
322+
List,
323+
ReferenceField,
324+
ReferenceInput,
325+
TextField,
326+
TextInput,
327+
} from 'react-admin';
328+
329+
const BookListAside = () => (
330+
<Card sx={{ order: -1, mr: 2, mt: 6, width: 250, height: 'fit-content' }}>
331+
<CardContent>
332+
<FilterList label="Century" icon={<CategoryIcon />}>
333+
<FilterListItem
334+
label="21st"
335+
value={{ year_gte: 2000, year_lte: undefined }}
336+
/>
337+
<FilterListItem
338+
label="20th"
339+
value={{ year_gte: 1900, year_lte: 1999 }}
340+
/>
341+
<FilterListItem
342+
label="19th"
343+
value={{ year_gte: 1800, year_lte: 1899 }}
344+
/>
345+
</FilterList>
346+
<FilterListSection label="Title" icon={<TitleIcon />}>
347+
<FilterLiveForm>
348+
<TextInput source="title" resettable helperText={false} />
349+
</FilterLiveForm>
350+
</FilterListSection>
351+
<FilterListSection label="Author" icon={<Person2Icon />}>
352+
<FilterLiveForm>
353+
<ReferenceInput source="authorId" reference="authors">
354+
<AutocompleteInput helperText={false} />
355+
</ReferenceInput>
356+
</FilterLiveForm>
357+
</FilterListSection>
358+
</CardContent>
359+
</Card>
360+
);
361+
362+
export const BookList = () => (
363+
<List aside={<BookListAside />}>
364+
<Datagrid>
365+
{/* ... */}
366+
</Datagrid>
367+
</List>
368+
);
369+
```
370+
{% endraw %}
371+
372+
![FilterLiveForm](./img/FilterLiveForm.png)
373+
374+
Check out the [`<FilterLiveForm>` documentation](./FilterLiveForm.md) for more information.

docs/FilterLiveForm.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
layout: default
3+
title: "FilterLiveForm"
4+
---
5+
6+
# `<FilterLiveForm>`
7+
8+
This component offers a convenient way to create a form that automatically updates the filters when the user changes its child input values.
9+
10+
It fits nicely alongside a [`<FilterList>`](./FilterList.md) component, but you can also use it at other places to create your own filter UI.
11+
12+
<video controls autoplay playsinline muted loop>
13+
<source src="./img/FilterLiveForm.mp4" type="video/mp4"/>
14+
Your browser does not support the video tag.
15+
</video>
16+
17+
## Usage
18+
19+
Use `<FilterLiveForm>` inside a component that provides a [`ListContext`](./useListContext.md), such as [`<List>`](./List.md). Use any React Admin [input component](./Inputs.md) as its children.
20+
21+
Here is an example showing how you can use `<FilterLiveForm>` in a sidebar for the `<List>` view, alongside a [`<FilterList>`](./FilterList.md):
22+
23+
{% raw %}
24+
```tsx
25+
import * as React from 'react';
26+
import CategoryIcon from '@mui/icons-material/LocalOffer';
27+
import Person2Icon from '@mui/icons-material/Person2';
28+
import TitleIcon from '@mui/icons-material/Title';
29+
import { Card, CardContent } from '@mui/material';
30+
import {
31+
AutocompleteInput,
32+
FilterLiveForm,
33+
Datagrid,
34+
FilterList,
35+
FilterListItem,
36+
FilterListSection,
37+
List,
38+
ReferenceField,
39+
ReferenceInput,
40+
TextField,
41+
TextInput,
42+
} from 'react-admin';
43+
44+
const BookListAside = () => (
45+
<Card sx={{ order: -1, mr: 2, mt: 6, width: 250, height: 'fit-content' }}>
46+
<CardContent>
47+
<FilterList label="Century" icon={<CategoryIcon />}>
48+
<FilterListItem
49+
label="21st"
50+
value={{ year_gte: 2000, year_lte: undefined }}
51+
/>
52+
<FilterListItem
53+
label="20th"
54+
value={{ year_gte: 1900, year_lte: 1999 }}
55+
/>
56+
<FilterListItem
57+
label="19th"
58+
value={{ year_gte: 1800, year_lte: 1899 }}
59+
/>
60+
</FilterList>
61+
<FilterListSection label="Title" icon={<TitleIcon />}>
62+
<FilterLiveForm>
63+
<TextInput source="title" resettable helperText={false} />
64+
</FilterLiveForm>
65+
</FilterListSection>
66+
<FilterListSection label="Author" icon={<Person2Icon />}>
67+
<FilterLiveForm>
68+
<ReferenceInput source="authorId" reference="authors">
69+
<AutocompleteInput helperText={false} />
70+
</ReferenceInput>
71+
</FilterLiveForm>
72+
</FilterListSection>
73+
</CardContent>
74+
</Card>
75+
);
76+
77+
export const BookList = () => (
78+
<List aside={<BookListAside />}>
79+
<Datagrid>
80+
<TextField source="title" />
81+
<ReferenceField source="authorId" reference="authors" />
82+
<TextField source="year" />
83+
</Datagrid>
84+
</List>
85+
);
86+
```
87+
{% endraw %}
88+
89+
**Tip:** This example leverages `<FilterListSection>`, the wrapper used internally by `<FilterList>`, in order to obtain a consistent look and feel for the filters.
90+
91+
![FilterLiveForm](./img/FilterLiveForm.png)
92+
93+
**Tip:** `<FilterLiveForm>` accepts multiple children, but you can also use several `<FilterLiveForm>` components in the same filter UI, just like we did above.
94+
95+
**Tip:** For simple cases where you only need a text input, you can use the [`<FilterLiveSearch>`](./FilterLiveSearch.md) component, which combines that logic in a single component.
96+
97+
## Props
98+
99+
Here are all the props you can set on the `<FilterLiveForm>` component:
100+
101+
| Prop | Required | Type | Default | Description |
102+
| --------------- | -------- | ------------------- | -------------------- | ------------------------------------------------------------------------ |
103+
| `children` | Required | `ReactNode` | - | The children of the filter form (usually inputs) |
104+
| `formComponent` | Optional | React Component | Native HTML `<form>` | A React Component used to render the form |
105+
| `debounce` | Optional | `number` or `false` | 500 | The debounce delay to set the filters (pass `false` to disable debounce) |
106+
| `validate` | Optional | `function` | - | A function to validate the form values |
107+
108+
Additional props are passed to `react-hook-form`'s [`useForm` hook](https://react-hook-form.com/docs/useform).
109+
110+
## `children`
111+
112+
`<FilterLiveForm>` accepts any children. It simply provides the required contexts for the inputs to work as filters.
113+
114+
```tsx
115+
<FilterLiveForm>
116+
<TextInput source="title" resettable helperText={false} />
117+
<TextInput source="author" resettable helperText={false} />
118+
</FilterLiveForm>
119+
```
120+
121+
## `debounce`
122+
123+
You can use the `debounce` prop to customize the delay before the filters are applied. The default value is `500` milliseconds.
124+
125+
```tsx
126+
<FilterLiveForm debounce={1000}>
127+
<TextInput source="title" resettable helperText={false} />
128+
<TextInput source="author" resettable helperText={false} />
129+
</FilterLiveForm>
130+
```
131+
132+
You can also disable the debounce by setting the `debounce` prop to `false`.
133+
134+
```tsx
135+
<FilterLiveForm debounce={false}>
136+
<TextInput source="title" resettable helperText={false} />
137+
<TextInput source="author" resettable helperText={false} />
138+
</FilterLiveForm>
139+
```
140+
141+
## `validate`
142+
143+
Just like for [`<Form>`](./Form.md), you can provide a `validate` function to validate the form values.
144+
145+
```tsx
146+
const validateFilters = values => {
147+
const errors: any = {};
148+
if (!values.author) {
149+
errors.author = 'The author is required';
150+
}
151+
return errors;
152+
};
153+
154+
const GlobalValidation = () => (
155+
<FilterLiveForm validate={validateFilters}>
156+
<TextInput source="title" resettable helperText={false} />
157+
<TextInput source="author" resettable helperText={false} />
158+
</FilterLiveForm>
159+
);
160+
```

docs/FilterLiveSearch.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ title: "The FilterLiveSearch Component"
1212
</video>
1313

1414

15-
The filter sidebar is not a form. Therefore, if your users need to enter complex filters, you'll have to recreate a filter form using react-hook-form (see the [Building a custom filter](./FilteringTutorial.md#building-a-custom-filter) for an example). However, if you only need one text input with a filter-as-you-type behavior, you'll find the `<FilterLiveSearch>` component convenient.
15+
The filter sidebar is not a form. Therefore, if your users need to enter complex filters, you'll have to recreate a filter form. This can be done thanks to the [`<FilterLiveForm>`](./FilterLiveForm.md) component. However, if you only need one text input with a filter-as-you-type behavior, you'll find the `<FilterLiveSearch>` component even more convenient.
1616

1717
It outputs a form containing a single `<TextInput>`, which modifies the page filter on change. That's usually what users expect for a full-text filter.
1818

@@ -55,4 +55,76 @@ export const CustomerList = () => (
5555
| `source` | Optional | `string` | 'q' | The field to filter on. |
5656
| `variant` | Optional | `string` | 'standard' | The variant of the search input. Can be one of 'standard', 'outlined', or 'filled'. |
5757

58-
Additional props are passed down to [the Material UI `<TextField>` component](https://mui.com/material-ui/api/text-field/).
58+
Additional props are passed down to [the Material UI `<TextField>` component](https://mui.com/material-ui/api/text-field/).
59+
60+
## Using Your Own Input
61+
62+
If the text input provided by `<FilterLiveSearch>` is not enough, and you'd like to use your own input component, you can use the `<FilterLiveForm>` component to create a form that automatically updates the filters when the user changes the input value.
63+
64+
{% raw %}
65+
```tsx
66+
import * as React from 'react';
67+
import CategoryIcon from '@mui/icons-material/LocalOffer';
68+
import Person2Icon from '@mui/icons-material/Person2';
69+
import TitleIcon from '@mui/icons-material/Title';
70+
import { Card, CardContent } from '@mui/material';
71+
import {
72+
AutocompleteInput,
73+
FilterLiveForm,
74+
Datagrid,
75+
FilterList,
76+
FilterListItem,
77+
FilterListSection,
78+
List,
79+
ReferenceField,
80+
ReferenceInput,
81+
TextField,
82+
TextInput,
83+
} from 'react-admin';
84+
85+
const BookListAside = () => (
86+
<Card sx={{ order: -1, mr: 2, mt: 6, width: 250, height: 'fit-content' }}>
87+
<CardContent>
88+
<FilterList label="Century" icon={<CategoryIcon />}>
89+
<FilterListItem
90+
label="21st"
91+
value={{ year_gte: 2000, year_lte: undefined }}
92+
/>
93+
<FilterListItem
94+
label="20th"
95+
value={{ year_gte: 1900, year_lte: 1999 }}
96+
/>
97+
<FilterListItem
98+
label="19th"
99+
value={{ year_gte: 1800, year_lte: 1899 }}
100+
/>
101+
</FilterList>
102+
<FilterListSection label="Title" icon={<TitleIcon />}>
103+
<FilterLiveForm>
104+
<TextInput source="title" resettable helperText={false} />
105+
</FilterLiveForm>
106+
</FilterListSection>
107+
<FilterListSection label="Author" icon={<Person2Icon />}>
108+
<FilterLiveForm>
109+
<ReferenceInput source="authorId" reference="authors">
110+
<AutocompleteInput helperText={false} />
111+
</ReferenceInput>
112+
</FilterLiveForm>
113+
</FilterListSection>
114+
</CardContent>
115+
</Card>
116+
);
117+
118+
export const BookList = () => (
119+
<List aside={<BookListAside />}>
120+
<Datagrid>
121+
{/* ... */}
122+
</Datagrid>
123+
</List>
124+
);
125+
```
126+
{% endraw %}
127+
128+
![FilterLiveForm](./img/FilterLiveForm.png)
129+
130+
Check out the [`<FilterLiveForm>` documentation](./FilterLiveForm.md) for more information.

docs/FilteringTutorial.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,48 @@ Normally, `showFilter()` adds one input to the `displayedFilters` list. As the f
522522

523523
### Custom Filter Form
524524

525-
Next is the filter form component, displayed only when the "main" filter is displayed (i.e. when a user has clicked the filter button). The form inputs appear directly in the form, and the form submission triggers the `setFilters()` callback passed as parameter. We'll use `react-hook-form` to handle the form state:
525+
If you need to build a custom filter form, you can use the [`<FilterLiveForm>`](./FilterLiveForm.md) component to create a form that automatically updates the filters when the user changes the input value.
526+
527+
{% raw %}
528+
```jsx
529+
import * as React from 'react';
530+
import { Box, InputAdornment } from '@mui/material';
531+
import SearchIcon from '@mui/icons-material/Search';
532+
import { FilterLiveForm, TextInput, NullableBooleanInput } from 'react-admin';
533+
534+
const PostFilterForm = () => (
535+
<FilterLiveForm>
536+
<Box display="flex" alignItems="flex-end" mb={1}>
537+
<Box component="span" mr={2}>
538+
{/* Full-text search filter. We don't use <SearchFilter> to force a large form input */}
539+
<TextInput
540+
resettable
541+
helperText={false}
542+
source="q"
543+
label="Search"
544+
InputProps={{
545+
endAdornment: (
546+
<InputAdornment>
547+
<SearchIcon color="disabled" />
548+
</InputAdornment>
549+
)
550+
}}
551+
/>
552+
</Box>
553+
<Box component="span" mr={2}>
554+
{/* Commentable filter */}
555+
<NullableBooleanInput
556+
helperText={false}
557+
source="commentable"
558+
/>
559+
</Box>
560+
</Box>
561+
</FilterLiveForm>
562+
);
563+
```
564+
{% endraw %}
565+
566+
If, instead, you want to control the form submission yourself, you can use the `useForm` hook from `react-hook-form`, and leverage the [filter callbacks](#filter-callbacks) from the `ListContext`:
526567

527568
{% raw %}
528569
```jsx

0 commit comments

Comments
 (0)