diff --git a/docgen/src/guide/Conditional_display.md b/docgen/src/guide/Conditional_display.md index da3063393b..7d4a0c6356 100644 --- a/docgen/src/guide/Conditional_display.md +++ b/docgen/src/guide/Conditional_display.md @@ -6,55 +6,45 @@ category: guide navWeight: 40 --- -Using our connector and [`createConnector`](guide/Custom_connectors.html) approach, you can conditionally display content based on the search state. +When no results are found you might want to display some specific contents helping the user go back +to a search that was successful. + +To help you do conditional rendering based on the `searchState` and the +`searchResults` of InstantSearch, we provide the [`connectStateResults`](connectors/connectStateResults.html) connector. ## Displaying content when the query is empty ```jsx -const Content = createConnector({ - displayName: 'ConditionalQuery', - getProvidedProps(props, searchState) { - return {query: searchState.query}; - }, - })(({query}) => { - const content = query - ?
The query {query} exists
- :
No query
; - return
{content}
; - }); +const Content = connectStateResults( + ({ searchState }) => + searchState && searchState.query + ?
+ The query {searchState.query} exists +
+ :
No query
+); ``` ## Displaying content when there's no results ```jsx -const content = createConnector({ - displayName: 'ConditionalResults', - getProvidedProps(props, searchState, searchResults) { - const noResults = searchResults.results ? searchResults.results.nbHits === 0 : false; - return {query: searchState.query, noResults}; - }, - })(({noResults, query}) => { - const content = noResults - ?
No results found for {query}
- :
Some results
; - return
{content}
; - }); +const Content = connectStateResults( + ({ searchState, searchResults }) => + searchResults && searchResults.nbHits !== 0 + ?
Some results
+ :
+ No results has been found for {searchState.query} +
+); ``` ## Displaying content when there's an error ```jsx -const content = createConnector({ - displayName: 'ConditionalError', - getProvidedProps(props, searchState, searchResults) { - return {error: searchResults.error}; - }, - })(({error}) => { - const content = error - ?
An error occurred: {error.message}
- :
Some results
; - return
{content}
; - }); +const Content = connectStateResults( + ({ error }) => + error ?
Some error
:
No error
+); ``` ## Displaying content when loading @@ -62,35 +52,93 @@ const content = createConnector({ In slow user network situations you might want to know when the search results are loading. ```jsx -const content = createConnector({ - displayName: 'ConditionalError', - getProvidedProps(props, searchState, searchResults) { - return {loading: searchResults.searching}; - }, -})(({loading}) => { - const content = loading - ?
We are loading
- :
Search finished
; - return
{content}
; - }); +const Content = connectStateResults( + ({ searching }) => + searching ?
We are searching
:
Search finished
+); ``` Alternatively, if you're using the search in List feature then you can know when the search results are loading by doing: ```jsx -const content = createConnector({ - displayName: 'ConditionalError', - getProvidedProps(props, searchState, searchResults) { - return {loading: searchResults.searchingForFacetValues}; - }, -})(({loading}) => { - const content = loading - ?
We are loading
- :
Search finished
; - return
{content}
; - }); +const Content = connectStateResults( + ({ searchingForFacetValues }) => + searchingForFacetValues ?
We are searching
:
Search finished
+); +``` + +## Conditional display when dealing with multi indices + +If you're using the `` API and want to apply some conditional rendering you have access to the `searchResults` but also to all the results of every used indices looking at `allSearchResults`. + +```jsx +const App = () => ( + + + +
+ + +
+
first:
+ +
+
+
+ + +
+
second:
+ +
+
+
+ + +
+
third:
+ +
+
+
+
+
+
+); + +const IndexResults = connectStateResults( + ({ searchState, searchResults, children }) => + searchResults && searchResults.nbHits !== 0 ? ( + children + ) : ( +
+ No results has been found for {searchState.query} and index{' '} + {searchResults ? searchResults.index : ''} +
+ ) +); + +const AllResults = connectStateResults(({ allSearchResults, children }) => { + const noResults = + allSearchResults && + Object.values(allSearchResults).reduce( + (acc, results) => results.nbHits === 0, + false + ); + return noResults ? ( +
+
No results in category, products or brand
+ + + +
+ ) : ( + children + ); +}); ``` +
Previous: ← Custom Connectors diff --git a/packages/react-instantsearch/connectors.js b/packages/react-instantsearch/connectors.js index e259663f28..6817785b8f 100644 --- a/packages/react-instantsearch/connectors.js +++ b/packages/react-instantsearch/connectors.js @@ -46,3 +46,6 @@ export { export { default as connectSortBy } from './src/connectors/connectSortBy.js'; export { default as connectStats } from './src/connectors/connectStats.js'; export { default as connectToggle } from './src/connectors/connectToggle.js'; +export { + default as connectStateResults, +} from './src/connectors/connectStateResults.js'; diff --git a/packages/react-instantsearch/src/connectors/connectHits.js b/packages/react-instantsearch/src/connectors/connectHits.js index 3d2d6bc9c0..985e464258 100644 --- a/packages/react-instantsearch/src/connectors/connectHits.js +++ b/packages/react-instantsearch/src/connectors/connectHits.js @@ -19,8 +19,9 @@ import { getResults } from '../core/indexUtils'; * @example * import React from 'react'; * - * import { connectHits, Highlight, InstantSearch } from 'react-instantsearch/dom'; - * + * import { Highlight, InstantSearch } from 'react-instantsearch/dom'; + * import { connectHits } from 'react-instantsearch/connectors'; + * const CustomHits = connectHits(({ hits }) => *
* {hits.map(hit => diff --git a/packages/react-instantsearch/src/connectors/connectStateResults.js b/packages/react-instantsearch/src/connectors/connectStateResults.js new file mode 100644 index 0000000000..e293f862a0 --- /dev/null +++ b/packages/react-instantsearch/src/connectors/connectStateResults.js @@ -0,0 +1,62 @@ +import createConnector from '../core/createConnector'; +import { getResults } from '../core/indexUtils'; + +/** + * The `connectStateResults` connector provides a way to access the `searchState` and the `searchResults` + * of InstantSearch. + * For instance this connector allows you to create results/noResults or query/noQuery pages. + * @name connectStateResults + * @kind connector + * @providedPropType {object} searchState - The search state of the instant search component.

See: [Search state structure](https://community.algolia.com/react-instantsearch/guide/Search_state.html) + * @providedPropType {object} searchResults - The search results.

In case of multiple indices: if used under ``, results will be those of the corresponding index otherwise it'll be those of the root index See: [Search results structure](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchresults) + * @providedPropType {object} allSearchResults - In case of multiple indices you can retrieve all the results + * @providedPropType {string} error - If the search failed, the error will be logged here. + * @providedPropType {boolean} searching - If there is a search in progress. + * @providedPropType {boolean} searchingForFacetValues - If there is a search in a list in progress. + * @example + * import React from 'react'; + * + * import { InstantSearch, Hits } from 'react-instantsearch/dom'; + * import { connectStateResults } from 'react-instantsearch/connectors'; + * + * const Content = connectStateResults( + * ({ searchState, searchResults }) => + * searchResults && searchResults.nbHits !== 0 + * ? + * :
+ * No results has been found for {searchState.query} + *
+ * ); + * return ( + * + * + * + * ); + * + * export default function App() { + * return ( + * + * + * + * ); + * } + */ +export default createConnector({ + displayName: 'AlgoliaStateResults', + + getProvidedProps(props, searchState, searchResults) { + const results = getResults(searchResults, this.context); + return { + searchState, + searchResults: results, + allSearchResults: searchResults.results, + searching: searchResults.searching, + error: searchResults.error, + searchingForFacetValues: searchResults.searchingForFacetValues, + }; + }, +}); diff --git a/packages/react-instantsearch/src/connectors/connectStateResults.test.js b/packages/react-instantsearch/src/connectors/connectStateResults.test.js new file mode 100644 index 0000000000..ad04071e6c --- /dev/null +++ b/packages/react-instantsearch/src/connectors/connectStateResults.test.js @@ -0,0 +1,69 @@ +/* eslint-env jest, jasmine */ + +import connect from './connectStateResults'; +jest.mock('../core/createConnector'); + +let props; + +describe('connectStateResults', () => { + describe('single index', () => { + const context = { context: { ais: { mainTargetedIndex: 'index' } } }; + const getProvidedProps = connect.getProvidedProps.bind(context); + it('provides the correct props to the component', () => { + const searchState = { state: 'state' }; + const error = 'error'; + const searching = true; + const searchingForFacetValues = true; + const searchResults = { + results: { nbHits: 25, hits: [] }, + error, + searching, + searchingForFacetValues, + }; + + props = getProvidedProps({}, searchState, searchResults); + expect(props).toEqual({ + searchState, + searchResults: searchResults.results, + allSearchResults: searchResults.results, + error, + searching, + searchingForFacetValues, + }); + }); + }); + describe('multi index', () => { + const context = { + context: { + ais: { mainTargetedIndex: 'first' }, + multiIndexContext: { targetedIndex: 'first' }, + }, + }; + const getProvidedProps = connect.getProvidedProps.bind(context); + it('provides the correct props to the component', () => { + const searchState = { state: 'state' }; + const error = 'error'; + const searching = true; + const searchingForFacetValues = true; + const searchResults = { + results: { + first: { nbHits: 25, hits: [] }, + second: { nbHits: 25, hits: [] }, + }, + error, + searching, + searchingForFacetValues, + }; + + props = getProvidedProps({}, searchState, searchResults); + expect(props).toEqual({ + searchState, + searchResults: searchResults.results.first, + allSearchResults: searchResults.results, + error, + searching, + searchingForFacetValues, + }); + }); + }); +}); diff --git a/packages/react-instantsearch/src/core/createConnector.js b/packages/react-instantsearch/src/core/createConnector.js index 0af7c1d880..b75ddba38d 100644 --- a/packages/react-instantsearch/src/core/createConnector.js +++ b/packages/react-instantsearch/src/core/createConnector.js @@ -272,6 +272,32 @@ export default function createConnector(connectorDesc) { } : {}; + if (process.env.NODE_ENV === 'development') { + const onlyGetProvidedPropsUsage = !Object.keys(connectorDesc).find( + key => + [ + 'getMetadata', + 'getSearchParameters', + 'refine', + 'cleanUp', + ].indexOf(key) > -1 + ); + + if ( + onlyGetProvidedPropsUsage && + !connectorDesc.displayName.startsWith('Algolia') + ) { + // eslint-disable-next-line no-console + console.warn( + 'react-instantsearch: it seems that you are using the `createConnector` api ' + + 'only to access the `searchState` and the `searchResults` through `getProvidedProps`.' + + 'We are now provided a dedicated API' + + ' the `connectStateResults` connector that you should use instead. The `createConnector` API will be ' + + 'soon deprecated and will break in future next major versions.' + ); + } + } + return ( { - const Content = createConnector({ - displayName: 'ConditionalResults', - getProvidedProps(props, searchState, searchResults) { - const noResults = searchResults.results - ? searchResults.results.nbHits === 0 - : false; - return { query: searchState.query, noResults }; - }, - })(({ noResults, query }) => { - const content = noResults ? ( -
No results has been found for {query}
- ) : ( -
Some results
- ); - return
{content}
; - }); + const Content = connectStateResults( + ({ searchState, searchResults }) => + searchResults && searchResults.nbHits !== 0 ? ( +
Some results
+ ) : ( +
No results has been found for {searchState.query}
+ ) + ); return ( @@ -30,19 +22,25 @@ stories ); }) .add('NoQuery/HasQuery', () => { - const Content = createConnector({ - displayName: 'ConditionalQuery', - getProvidedProps(props, searchState) { - return { query: searchState.query }; - }, - })(({ query }) => { - const content = query ? ( -
The query {query} exists
- ) : ( -
No query
- ); - return
{content}
; - }); + const Content = connectStateResults( + ({ searchState }) => + searchState && searchState.query ? ( +
The query {searchState.query} exists
+ ) : ( +
No query
+ ) + ); + return ( + + + + ); + }) + .add('NoLoading/HasQuery', () => { + const Content = connectStateResults( + ({ searching }) => + searching ?
searching
:
No searching
+ ); return ( diff --git a/stories/MultiIndex.stories.js b/stories/MultiIndex.stories.js index 425023bad4..0cb0c0ee16 100644 --- a/stories/MultiIndex.stories.js +++ b/stories/MultiIndex.stories.js @@ -11,8 +11,10 @@ import { import { connectHits, connectAutoComplete, + connectStateResults, } from '../packages/react-instantsearch/connectors'; import Autosuggest from 'react-autosuggest'; + const stories = storiesOf('', module); stories @@ -59,6 +61,50 @@ stories + )) + .add('with conditional rendering', () => ( + + + +
+
+ + +
+
Categories:
+ + +
+
+
+ + +
+
Brand:
+ + +
+
+
+
+
+ + +
+
Products:
+ + +
+
+
+
+
+
+
)); const AutoComplete = connectAutoComplete( @@ -129,3 +175,34 @@ const Product = ({ hit }) => { Product.propTypes = { hit: PropTypes.object.isRequired, }; + +const Content = connectStateResults( + ({ searchState, searchResults, children }) => + searchResults && searchResults.nbHits !== 0 ? ( + children + ) : ( +
+ No results has been found for {searchState.query} and index{' '} + {searchResults ? searchResults.index : ''} +
+ ) +); + +const Results = connectStateResults(({ allSearchResults, children }) => { + const noResults = + allSearchResults && + Object.values(allSearchResults).reduce( + (acc, results) => results.nbHits === 0, + false + ); + return noResults ? ( +
+
No results in category, products or brand
+ + + +
+ ) : ( + children + ); +});