diff --git a/docgen/src/examples/Recipes.md b/docgen/src/examples/Recipes.md index 090af8ab0a..2410ab6a0c 100644 --- a/docgen/src/examples/Recipes.md +++ b/docgen/src/examples/Recipes.md @@ -20,6 +20,9 @@ examples: [{ }, { id: 'react-router-v4', title: 'Usage with react-router v4.' + }, { + id: 'geo-search', + title: 'Geo search using dynamic search parameters' }] examplesEndpoint: https://github.com/algolia/instantsearch.js/tree/v2/packages/react-instantsearch/examples --- diff --git a/docgen/src/guide/Search_parameters.md b/docgen/src/guide/Search_parameters.md index 6d1b566f4d..f9a76cc5b8 100644 --- a/docgen/src/guide/Search_parameters.md +++ b/docgen/src/guide/Search_parameters.md @@ -26,6 +26,15 @@ Here's an example configuring the [distinct parameter](https://www.algolia.com/d * You could also pass `hitsPerPage: 20` to configure the number of hits being shown when not using the [`` widget](widgets/HitsPerPage.html). +## Dynamic search parameters updates + +Every applied search parameters can be retrieved by listening to the `onSearchStateChange` +hook from the [``](guide/.html) root component. + +But to update the search parameters, you will need to pass updated props to the `` widget, directly modifying the search `state` prop and injecting it will have no effect. + +[Read the example](https://github.com/algolia/instantsearch.js/tree/v2/packages/react-instantsearch/examples/geo-search) performing geo-search with `react-instantsearch` to see how you can update search parameters. +
Previous: ← Routing diff --git a/docgen/src/images/examples/geo-search.png b/docgen/src/images/examples/geo-search.png new file mode 100644 index 0000000000..5813233972 Binary files /dev/null and b/docgen/src/images/examples/geo-search.png differ diff --git a/packages/react-instantsearch/examples/geo-search/README.md b/packages/react-instantsearch/examples/geo-search/README.md new file mode 100644 index 0000000000..c009bdc8ea --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/README.md @@ -0,0 +1,10 @@ +This example shows how to perform a geo search using `react-instantsearch`. + +To start the example: + +```sh +yarn install --no-lockfile +yarn start +``` + +Read more about `react-instantsearch` [in our documentation](https://community.algolia.com/instantsearch.js/react/). diff --git a/packages/react-instantsearch/examples/geo-search/package.json b/packages/react-instantsearch/examples/geo-search/package.json new file mode 100644 index 0000000000..879cf37a00 --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "devDependencies": { + "react-scripts": "^0.8.5" + }, + "dependencies": { + "google-map-react": "^0.22.3", + "qs": "^6.3.0", + "react": "^15.4.2", + "react-dom": "^15.4.2", + "react-instantsearch": "latest", + "react-instantsearch-theme-algolia": "latest" + }, + "scripts": { + "start": "react-scripts start" + }, + "main": "index.js", + "license": "MIT" +} diff --git a/packages/react-instantsearch/examples/geo-search/public/index.html b/packages/react-instantsearch/examples/geo-search/public/index.html new file mode 100644 index 0000000000..76df3cb688 --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/public/index.html @@ -0,0 +1,12 @@ + + + + + + geo-search with react-instantsearch + + + +
+ + diff --git a/packages/react-instantsearch/examples/geo-search/public/style.css b/packages/react-instantsearch/examples/geo-search/public/style.css new file mode 100644 index 0000000000..1402f95f10 --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/public/style.css @@ -0,0 +1,24 @@ +body { + font-family: Roboto; + font-size: 16px; + color: #565A5C; +} + +.map{ + padding-right: 0; + padding-left: 0; + height: 700px; +} + +.marker { + transform: translate(-50%, -100%) scale(0.5, 0.5); +} + +.ais-InstantSearch__root { + display: flex; + flex-direction: column; +} + +.ais-SearchBox__root { + margin: 10px 0; +} \ No newline at end of file diff --git a/packages/react-instantsearch/examples/geo-search/src/App.js b/packages/react-instantsearch/examples/geo-search/src/App.js new file mode 100644 index 0000000000..49c63958c5 --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/src/App.js @@ -0,0 +1,143 @@ +import { + InstantSearch, + SearchBox, + Configure, +} from 'react-instantsearch/dom'; +import { + connectHits, +} from 'react-instantsearch/connectors'; + +import React, {PropTypes} from 'react'; +import GoogleMap from 'google-map-react'; +import {fitBounds} from 'google-map-react/utils'; +import 'react-instantsearch-theme-algolia/style.css'; +import qs from 'qs'; + +const updateAfter = 700; +const searchStateToUrl = + searchState => + searchState ? `${window.location.pathname}?${qs.stringify(searchState)}` : ''; + +class App extends React.Component { + constructor() { + super(); + + // retrieve searchState and aroundLatLng properties from the URL at first rendering + const initialSearchState = qs.parse(window.location.search.slice(1)); + const aroundLatLng = initialSearchState.configure ? initialSearchState.configure.aroundLatLng : null; + this.state = {searchState: initialSearchState, aroundLatLng}; + + this.onSearchStateChange = this.onSearchStateChange.bind(this); + this.onLatLngChange = this.onLatLngChange.bind(this); + window.addEventListener( + 'popstate', + ({state: searchState}) => this.setState({searchState}) + ); + } + + // when a click on the map is performed, the query is reset and the aroundLatLng property updated. + onLatLngChange = ({lat, lng}) => { + this.setState({searchState: {...this.state.searchState, query: ''}, aroundLatLng: `${lat},${lng}`}); + }; + + onSearchStateChange = searchState => { + // update the URL when there is a new search state. + clearTimeout(this.debouncedSetState); + this.debouncedSetState = setTimeout(() => { + window.history.pushState( + searchState, + null, + searchStateToUrl(searchState) + ); + }, updateAfter); + + // when a new query is performed, removed the aroundLatLng property. + const aroundLatLng = this.state.searchState.query !== searchState.query ? null : this.state.aroundLatLng; + this.setState({searchState, aroundLatLng}); + }; + + render() { + const configuration = this.state.aroundLatLng + ? + : ; + return ( + + {configuration} + Either type a destination or click somewhere on the map to see the closest appartement. + +
+ +
+
+ ); + } +} + +function CustomMarker() { + /* eslint-disable max-len */ + return ( + + + + + + + + ); + /* eslint-enable max-len */ +} + +function HitsMap({hits, onLatLngChange}) { + const availableSpace = { + width: document.body.getBoundingClientRect().width * 5 / 12, + height: 400, + }; + const boundingPoints = hits.reduce((bounds, hit) => { + const pos = hit; + if (pos.lat > bounds.nw.lat) bounds.nw.lat = pos.lat; + if (pos.lat < bounds.se.lat) bounds.se.lat = pos.lat; + + if (pos.lng < bounds.nw.lng) bounds.nw.lng = pos.lng; + if (pos.lng > bounds.se.lng) bounds.se.lng = pos.lng; + return bounds; + }, { + nw: {lat: -85, lng: 180}, + se: {lat: 85, lng: -180}, + }); + const boundsConfig = hits.length > 0 ? fitBounds(boundingPoints, availableSpace) : {}; + const markers = hits.map(hit => ); + const options = { + minZoomOverride: true, + minZoom: 2, + }; + return ( + options} + bootstrapURLKeys={{ + key: 'AIzaSyAl60n7p07HYQK6lVilAe_ggwbBJFktNw8', + }} + center={boundsConfig.center} + zoom={boundsConfig.zoom} + onClick={onLatLngChange} + >{markers} + ); +} + +HitsMap.propTypes = { + hits: PropTypes.array, + onLatLngChange: PropTypes.func, +}; + +const ConnectedHitsMap = connectHits(HitsMap); + +export default App; diff --git a/packages/react-instantsearch/examples/geo-search/src/index.js b/packages/react-instantsearch/examples/geo-search/src/index.js new file mode 100644 index 0000000000..1c572c8bff --- /dev/null +++ b/packages/react-instantsearch/examples/geo-search/src/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/react-instantsearch/src/connectors/connectConfigure.js b/packages/react-instantsearch/src/connectors/connectConfigure.js index 8a8bce660a..b2047f73ae 100644 --- a/packages/react-instantsearch/src/connectors/connectConfigure.js +++ b/packages/react-instantsearch/src/connectors/connectConfigure.js @@ -1,31 +1,24 @@ import createConnector from '../core/createConnector.js'; -import {omit, isEmpty} from 'lodash'; +import {omit, isEmpty, difference, keys} from 'lodash'; const namespace = 'configure'; -function getCurrentRefinement(props, searchState) { - return Object.keys(props).reduce((acc, item) => { - acc[item] = searchState[namespace] && searchState[namespace][item] - ? searchState[namespace][item] : props[item]; - return acc; - }, {}); -} - export default createConnector({ displayName: 'AlgoliaConfigure', getProvidedProps() { return {}; }, - getSearchParameters(searchParameters, props, searchState) { + getSearchParameters(searchParameters, props) { const items = omit(props, 'children'); - const configuration = getCurrentRefinement(items, searchState); - return searchParameters.setQueryParameters(configuration); + return searchParameters.setQueryParameters(items); }, transitionState(props, prevSearchState, nextSearchState) { const items = omit(props, 'children'); + const nonPresentKeys = this._props ? difference(keys(this._props), keys(props)) : []; + this._props = props; return { ...nextSearchState, - [namespace]: {...items, ...nextSearchState[namespace]}, + [namespace]: {...omit(nextSearchState[namespace], nonPresentKeys), ...items}, }; }, cleanUp(props, searchState) { diff --git a/packages/react-instantsearch/src/connectors/connectConfigure.test.js b/packages/react-instantsearch/src/connectors/connectConfigure.test.js index 5cf3203834..e20a646d23 100644 --- a/packages/react-instantsearch/src/connectors/connectConfigure.test.js +++ b/packages/react-instantsearch/src/connectors/connectConfigure.test.js @@ -18,31 +18,22 @@ describe('connectConfigure', () => { expect(searchParameters.getQueryParameter.bind(searchParameters, 'children')).toThrow(); }); - it('configure parameters that are in the search state should override the default one', () => { - const searchParameters = getSearchParameters( - new SearchParameters(), - {distinct: 1, whatever: 'please', children: 'whatever'}, - {configure: {whatever: 'priority'}} - ); - expect(searchParameters.getQueryParameter('distinct')).toEqual(1); - expect(searchParameters.getQueryParameter('whatever')).toEqual('priority'); - expect(searchParameters.getQueryParameter.bind(searchParameters, 'children')).toThrow(); - }); - it('calling transitionState should add configure parameters to the search state', () => { - let searchState = transitionState( + const providedThis = {}; + let searchState = transitionState.call(providedThis, {distinct: 1, whatever: 'please', children: 'whatever'}, {}, {} ); expect(searchState).toEqual({configure: {distinct: 1, whatever: 'please'}}); - searchState = transitionState( - {distinct: 1, whatever: 'please', children: 'whatever'}, + searchState = transitionState.call(providedThis, + {whatever: 'other', children: 'whatever'}, + {configure: {distinct: 1, whatever: 'please'}}, {configure: {distinct: 1, whatever: 'please'}}, - {configure: {distinct: 1, whatever: 'whatever'}}, ); - expect(searchState).toEqual({configure: {distinct: 1, whatever: 'whatever'}}); + + expect(searchState).toEqual({configure: {whatever: 'other'}}); }); it('calling cleanUp should remove configure parameters from the search state', () => { diff --git a/packages/react-instantsearch/src/core/createConnector.js b/packages/react-instantsearch/src/core/createConnector.js index c2ac65119c..07e7b637f2 100644 --- a/packages/react-instantsearch/src/core/createConnector.js +++ b/packages/react-instantsearch/src/core/createConnector.js @@ -83,7 +83,7 @@ export default function createConnector(connectorDesc) { ) : null; const transitionState = hasTransitionState ? - (prevWidgetsState, nextWidgetsState) => connectorDesc.transitionState( + (prevWidgetsState, nextWidgetsState) => connectorDesc.transitionState.call(this, this.props, prevWidgetsState, nextWidgetsState @@ -107,7 +107,7 @@ export default function createConnector(connectorDesc) { // and getMetadata with the new props. this.context.ais.widgetsManager.update(); if (connectorDesc.transitionState) { - this.context.ais.onSearchStateChange(connectorDesc.transitionState( + this.context.ais.onSearchStateChange(connectorDesc.transitionState.call(this, nextProps, this.context.ais.store.getState().widgets, this.context.ais.store.getState().widgets,