Skip to content

Commit

Permalink
List - Optional Sync With Location
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Jan 19, 2021
1 parent 15cd871 commit 787e696
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 18 deletions.
37 changes: 37 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ The `<List>` component fetches the list of records from the data provider, and r

Here is the minimal code necessary to display a list of posts using a `<Datagrid>`:

* [`title`](#page-title)
* [`actions`](#actions)
* [`exporter`](#exporter)
* [`bulkActionButtons`](#bulk-action-buttons)
* [`filters`](#filters) (a React element used to display the filter form)
* [`filterDefaultValues`](#filter-default-values) (the default values for `alwaysOn` filters)
* [`perPage`](#records-per-page)
* [`sort`](#default-sort-field)
* [`filter`](#permanent-filter) (the permanent filter used in the REST request)
* [`pagination`](#pagination)
* [`aside`](#aside-component)
* [`empty`](#empty-page)
- [`syncWithLocation`](#synchronize-with-url)

```jsx
// in src/posts.js
import * as React from "react";
Expand Down Expand Up @@ -729,6 +743,29 @@ const PostList = props => (

The default value for the `component` prop is `Card`.

## Synchronize With URL

When a List based component (eg: `PostList`) is passed to the `list` prop of a `<Resource>`, it will automatically synchronize its parameters with the browser URL (using react-router location). However, when used anywhere outside of a `<Resource>`, it won't synchronize, which can be useful when you have multiple lists on a single page for example.

In order to enable the synchronization with the URL, you can set the `syncWithLocation` prop. For example, adding a `List` to an `Edit` page:

```jsx
const TagsEdit = (props) => (
<>
<Edit {...props}>
// ...
</Edit>
<ResourceProviderContext resource="posts">
<List syncWithLocation basePath="/posts" filter={{ tags: [id]}}>
<Datagri>
<TextField source="title" />
</Datagrid>
</List>
</ResourceProviderContext>
</>
)
```

### CSS API

The `List` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:
Expand Down
45 changes: 37 additions & 8 deletions examples/simple/src/tags/TagEdit.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
/* eslint react/jsx-key: off */
import * as React from 'react';
import { Edit, SimpleForm, TextField, TextInput, required } from 'react-admin';
import {
Edit,
SimpleForm,
TextField,
TextInput,
required,
List,
Datagrid,
ResourceContextProvider,
EditButton,
} from 'react-admin';

const TagEdit = props => (
<Edit {...props}>
<SimpleForm redirect="list">
<TextField source="id" />
<TextInput source="name" validate={[required()]} />
</SimpleForm>
</Edit>
<>
<Edit {...props}>
<SimpleForm redirect="list">
<TextField source="id" />
<TextInput source="name" validate={[required()]} />
</SimpleForm>
</Edit>
<ResourceContextProvider resource="posts">
<List
hasCreate={false}
hasShow
hasEdit
hasList
basePath="/posts"
resource="posts"
filter={{ tags: [props.id] }}
title=" "
>
<Datagrid>
<TextField source="id" />
<TextField source="title" />
<EditButton />
</Datagrid>
</List>
</ResourceContextProvider>
</>
);

export default TagEdit;
8 changes: 6 additions & 2 deletions packages/ra-core/src/controller/ListBase.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import useListController from './useListController';
import { ReactNode } from 'react';
import useListController, { ListProps } from './useListController';
import ListContextProvider from './ListContextProvider';

/**
Expand Down Expand Up @@ -36,7 +37,10 @@ import ListContextProvider from './ListContextProvider';
* </ListBase>
* );
*/
const ListBase = ({ children, ...props }) => (
const ListBase = ({
children,
...props
}: ListProps & { children: ReactNode }) => (
<ListContextProvider value={useListController(props)}>
{children}
</ListContextProvider>
Expand Down
5 changes: 5 additions & 0 deletions packages/ra-core/src/controller/useListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export interface ListProps {
location?: Location;
path?: string;
resource?: string;
// Wether to synchronize the list parameters with the current location (URL search parameters)
// This is set to true automatically when a List is used inside a Resource component
syncWithLocation: boolean;
[key: string]: any;
}

Expand Down Expand Up @@ -118,6 +121,7 @@ const useListController = <RecordType extends Record = Record>(
perPage = 10,
filter,
debounce = 500,
syncWithLocation,
} = props;
const resource = useResourceContext(props);

Expand All @@ -138,6 +142,7 @@ const useListController = <RecordType extends Record = Record>(
sort,
perPage,
debounce,
syncWithLocation,
});

const [selectedIds, selectionModifiers] = useRecordSelection(resource);
Expand Down
26 changes: 18 additions & 8 deletions packages/ra-core/src/controller/useListParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface ListParamsOptions {
// default value for a filter when displayed but not yet set
filterDefaultValues?: FilterPayload;
debounce?: number;
// Wether to synchronize the list parameters with the current location (URL search parameters)
// This is set to true automatically when a List is used inside a Resource component
syncWithLocation: boolean;
}

interface Parameters extends ListParams {
Expand Down Expand Up @@ -112,6 +115,7 @@ const useListParams = ({
sort = defaultSort,
perPage = 10,
debounce = 500,
syncWithLocation,
}: ListParamsOptions): [Parameters, Modifiers] => {
const dispatch = useDispatch();
const history = useHistory();
Expand All @@ -132,7 +136,9 @@ const useListParams = ({
perPage,
];

const queryFromLocation = parseQueryFromLocation(location);
const queryFromLocation = syncWithLocation
? parseQueryFromLocation(location)
: {};

const query = useMemo(
() =>
Expand All @@ -158,13 +164,17 @@ const useListParams = ({

const changeParams = useCallback(action => {
const newParams = queryReducer(query, action);
history.push({
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
displayedFilters: JSON.stringify(newParams.displayedFilters),
})}`,
});
if (syncWithLocation) {
history.push({
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
displayedFilters: JSON.stringify(
newParams.displayedFilters
),
})}`,
});
}
dispatch(changeListParams(resource, newParams));
}, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps

Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/core/Resource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const ResourceRoutes: FunctionComponent<ResourceProps> = ({
basePath={basePath}
{...routeProps}
{...resourceData}
syncWithLocation
/>
)}
/>
Expand Down
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/list/List.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('<List />', () => {
history: {} as any,
location: {} as any,
match: (() => {}) as any,
syncWithLocation: true,
};

const defaultStateForList = {
Expand Down
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ListProps extends ResourceComponentProps {
pagination?: ReactElement | false;
perPage?: number;
sort?: SortPayload;
syncWithLocation: boolean;
title?: string | ReactElement;
}

Expand Down

0 comments on commit 787e696

Please sign in to comment.