ra-search

react-admin ≥ 3.10.2

Plug your search engine and let users search across all resources via a smart Omnisearch box.

Installation

npm install --save @react-admin/ra-search
# or
yarn add @react-admin/ra-search

Configuring the dataProvider

dataProvider.search()

ra-search passes search queries to the dataProvider, which must contain a custom search method with the following signature:

type search = (query: string, options?: SearchOptions) => Promise<{ data: SearchResult[], total: number }>

interface SearchOptions {
    targets?: string[];
    [key: string]: any;
}

interface SearchResult {
    id: Identifier;
    type: string;
    url: string;
    content: any;
    matches?: any;
}

Here is an example of the expected syntax for dataProvider.search():

dataProvider.search('Hendrix', { targets : ['artists', 'albums'] })
    .then(response => console.log(response));
// {
//     data: [
//         { id: 'artists/1', type: 'artist', url: '/artists/1', content: { id: 1, label: 'Jimi Hendrix', description: 'James Marshall Hendrix'  } }
//         { id: 'albums/1', type: 'album', url: '/albums/1', content: { id: 1, label: 'Are You Experienced', description:  'The debut studio album', year: 1967 } }
//         { id: 'albums/2', type: 'album', url: '/albums/2', content: { id: 2, label: 'Axis: Bold as Love', description: 'The second studio album', year: 1967 } }
//     ],
//     total: 3
// }

It is your responsibility to add this search method to your dataProvider so that react-admin can send queries to and read responses from the search engine.

The dataProvider.search() method should return a Promise for data containing an array of SearchResult objects. A SearchResult contains at least the following fields:

  • id: Identifier The unique identifier of the search result
  • type: string An arbitrary string which enables grouping
  • url: string The url where to redirect to on click. It could be a custom page and not a resource if you want to
  • content: any Can contains any data that will be used to display the result. If used with default <SearchResultItem> component, it must contain at least an id, label and a description.
  • matches: any An optional object containing an extract of the data with matches. Can be anything that will be interpreted by a <SearchResultItem>

As for the total, it can be grater than the number of returned results. This is useful e.g. to show that there are more results.

addSearchMethod Helper

If you don't have a full-text search endpoint in your API, you can use the Simple addSearchMethod helper function. It adds the search() method to an existing dataProvider, reyling on the dataProvider.getList() method on the configured resources.

import simpleRestProvider from 'ra-data-simple-rest';
import { addSearchMethod } from '@react-admin/ra-search';

const dataProvider = simpleRestProvider('http://path.to.my.api/');

const dataProviderWithSearch = addSearchMethod(dataProvider, ['artists', 'albums']);

Now calling dataProvider.search('foo') will issue the following queries:

  • dataProvider.getList('artists', { filter: { q: "foo" }})
  • dataProvider.getList('albumns', { filter: { q: "foo" }})

Then aggregate the results and return them in a single response.

So the search() method created by this helper calls the regular dataProvider several times, once for each resource. We don't recommend using it in production - instead, you should modify your API to support the search method, e.g. by using a search engine, and implement your own dataProvider.search() method to convert the results to the format expected by ra-search.

The second argument to addSearchMethod is the builder configuraiton. It can be either an array of resources names or a map of the resources specifying how to format their records for search results.

When called with an array of resources, addSearchMethod populates the search results content based on the records returned by dataProvider.getList(), with the following inference rules:

  • id: Returns the record id
  • label: Returns the record label or name or title
  • description: Returns the record description or body

Example with an array of resources:

const dataProviderWithSearch = addSearchMethod(dataProvider, ['artists', 'albums']);

When called with a map, each key being a resource name, the value can have the following properties:

  • label: Either the field name to use as the label or a function which will be called with a record and must return a string. Defaults to the inference described above.
  • description: Either the field name to use as the description or a function which will be called with a record and must return a string. Defaults to the inference described above.

Example with a map of resources:

const dataProviderWithSearch = addSearchMethod(dataProvider, {
    artists: {
        label: 'full_name',
        description: record => `${record.born_at}-${record.died_at} ${record.biography}`,
    },
    albums: {
        // no label specified, fallback on inference
        description: record => `${record.released_at.getFullYear()} by ${record.recordCompany}`,
    },
});

The <Search> Component

The <Search> component includes an input and displays the search results inside a Material-UI PopOver.

By default, it will group the search results by target, and show their content.label and content.description.

Here's how to include it inside a custom <AppBar> component:

import { Admin, AppBar, Layout, Resource } from 'react-admin';
import { Typography, makeStyles } from '@material-ui/core';
import { Search } from '@react-admin/ra-search';

const MyAppbar = (props) => {
    const classes = useStyles();

    return (
        <AppBar {...props}>
            <Typography
                variant="h6"
                color="inherit"
                className={classes.title}
                id="react-admin-title"
            />
            <Search />
        </AppBar>
    );
};

const useStyles = makeStyles({
    title: {
        flex: 1,
        textOverflow: 'ellipsis',
        whiteSpace: 'nowrap',
        overflow: 'hidden',
    },
});

const MyLayout = (props) => (
    <Layout {...props} appBar={MyAppbar} />
);

export const App = () => (
    <Admin dataProvider={searchDataProvider} layout={MyLayout}>
        {...}
    </Admin>
);

The <Search> component accepts the following props:

  • targets: string[] An array of the indices on which to perform the search. Defaults to an empty array.
  • children: A component which will display the results inside the <Popover>. Defaults to <SearchResultsPanel>.
  • color: The color mode for the input, applying light or dark backgrounds. Accept either light or dark. Defaults to light.

Customizing The Result Items

By default, <Search> displays the results in <SearchResultsPanel>, which displays each result in a <SearchResultItem>. So rendering <Search /> without children is equivalent to rendering:

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem/>
        </SearchResultsPanel>
    </Search>
);

<SearchResultItem> renders the content.label and content.description for each result. You can customize what it renders by providing a function as the label and a the description props. This function takes the search result as parameter and must return a React element.

For instance:

import { Search, SearchResultsPanel, SearchResultItem } from '@react-admin/ra-search';

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <SearchResultItem 
                label={(record) => (
                    <>
                        {record.type === 'artists' ? (
                            <PersonIcon />
                        ) : (
                            <MusicIcon />
                        )}
                        <span>{record.content.label}</span>
                    </>
                )}
            />
        </SearchResultsPanel>
    </Search>
);

You can also completely replace the search result item component:

import { Search, SearchResultsPanel } from '@react-admin/ra-search';

const MySearchResultItem = ({ data, onClose }) => (
    <li key={data.id}>
        <Link to={data.url} onClick={onClose}>
            <strong>{data.content.label}</strong>
        </Link>
        <p>{data.content.description}</p>
    </li>
);

const MySearch = () => (
    <Search>
        <SearchResultsPanel>
            <MySearchResultItem />
        </SearchResultsPanel>
    </Search>
);

Customizing the Entire Search Results

Pass a custom React element as child of <Search> to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results in a different way.

ra-search renders the <Search> inside a SearchContext. You use the useSearchResultContext hook to read the search results, as follows:

import { Search, useSearchResultContext } from '@react-admin/ra-search';

const MySearch = (props) => (
    <Search>
        <CustomSearchResultsPanel />
    </Search>
);

const CustomSearchResultsPanel = () => {
    const { data, onClose } = useSearchResultContext();

    return (
        <ul>
            {data.map(searchResult => (
                <li key={searchResult.id}>
                    <Link to={searchResult.url} onClick={onClose}>
                        <strong>{searchResult.content.label}</strong>
                    </Link>
                    <p>{searchResult.content.description}</p>
                </li>
            ))}
        </ul>
    );
};

The useSearch Hook

Just like useMutation, useSearch returns a function allowing to call the dataProvidr.search(), as well as a state object for the response. Use it to create your own Search component.

import { useState } from 'React';
import { TextField } from '@material-ui/core';
import { useSearch, SearchResultContextProvider, SearchResultsPanel } from '@react-admin/ra-search';

const Search = () => {
    const [query, setQuery] = useState();
    const [search, searchResultState] = useSearch();

    const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
        setQuery(event.target.value);
        search(event.target.value);
    };

    return (
        <>
            <TextField value={query} onChange={handleChange} />
            <SearchResultContextProvider value={searchResultState}>
                <SearchResultsPanel />
            </SearchResultContextProvider>
        </>
    );
};

CHANGELOG

v2.0.1

2020-12-18

  • (fix) Fix search results panel markup and accessibility

v2.0.0

2020-12-17

  • (feat) rename search method options (facets -> targets)

v1.2.3

2020-12-08

  • (fix) Fix SearchInput value is not selectable while search results are displayed

v1.2.2

2020-12-03

  • (fix) Fix part of terms are sometimes discarded when response is slow
  • (fix) Fix empty popover is displayed when there are no more results

v1.2.1

2020-11-23

  • (fix) Fix search results popover closes and reopens at each keystroke

v1.2.0

2020-11-04

  • (feat) Export groupSearchResultsByResource function

v1.1.1

2020-10-19

  • (fix) Display the <PopOver> position at the bottom right of the <SearchInput>
  • (fix) Remove the stickyness of facet headers
  • (feat) Add a growing effect when focusing the menu

v1.1.0

2020-10-14

  • (fix) Hide clear button when there is no value in the <SearchInput>
  • (feat) Add color prop to both <SearchInput> and <Search> component supporting light and dark

v1.0.0

2020-10-13

  • First release