Skip to content

Commit

Permalink
feat(widgets): add transformItems to be able to sort and filter (#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
mthuret authored and bobylito committed Jan 10, 2017
1 parent 4accce5 commit ba539f0
Show file tree
Hide file tree
Showing 29 changed files with 362 additions and 129 deletions.
2 changes: 1 addition & 1 deletion docgen/src/guide/Connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Provided props always follow the same pattern for ease of use:

<div class="guide-nav">
<div class="guide-nav-left">
Previous: <a href="guide/i18n.html">← i18n</a>
Previous: <a href="guide/Sorting_and_filtering.html">← Sorting and filtering items</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Default_refinements.html">Default refinements →</a>
Expand Down
51 changes: 51 additions & 0 deletions docgen/src/guide/Sorting_and_filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: Sorting and filtering items
mainTitle: Guide
layout: main.pug
category: guide
navWeight: 78
---

A frequent question that comes up is "How do I sort or filter the items of my widget".

For this, widgets and connectors that are handling items expose a `transformItems` prop. This prop is a function that has the `items` provided
prop as a parameter. It will expect in return the `items` prop back.

This props can be found on every widgets or connectors that handle a list of `items`:
* [`<CurrentRefinements/>`](widgets/CurrentRefinements.html) and [`connectCurrentRefinements`](connectors/connectCurrentRefinements.html)
* [`<HierarchicalMenu/>`](widgets/HierarchicalMenu.html) and [`connectHierarchicalMenu`](connectors/connectHierarchicalMenu.html)
* [`<Menu/>`](widgets/Menu.html) and [`connectMenu`](connectors/connectMenu.html)
* [`<RefinementList/>`](widgets/RefinementList.html) and [`connectRefinementList`](connectors/connectRefinementList.html)
* [`<SortBy/>`](widgets/SortBy.html) and [`connectSortBy`](connectors/connectSortBy.html)
* [`<HitsPerPage/>`](widgets/HitsPerPage.html) and [`connectHitsPerPage`](connectors/connectHitsPerPage.html)
* [`<MultiRange/>`](widgets/MultiRange.html) and [`connectMultiRange`](connectors/connectMultiRange.html)

The following example will show you how to change the default sort order of the [`<RefinementList/>`](widgets/RefinementList.html) widget.

```jsx
import {InstantSearch, RefinementList} from 'react-instantsearch/dom';
import {orderBy} from 'lodash';

const App = () =>
<InstantSearch
appId="..."
apiKey="..."
indexName="..."
>
<SearchBox defaultRefinement="hi" />
<RefinementList attributeName="category"
transformItems={items => orderBy(items, ['label', 'count'], ['asc', 'desc'])}/>
</InstantSearch>;
```

**Notes:**
* The example shows how to sort items, but you can also filter them

<div class="guide-nav">
<div class="guide-nav-left">
Previous: <a href="guide/i18n.html">← i18n</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Connectors.html">Connectors →</a>
</div>
</div>
2 changes: 1 addition & 1 deletion docgen/src/guide/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ const App = () =>
Previous: <a href="guide/Highlighting_results.html">← Highlighting Results</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Connectors.html">Connectors →</a>
Next: <a href="guide/Sorting_and_filtering.html">Sorting and filtering items →</a>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CurrentRefinements extends Component {
label: PropTypes.string,
})).isRequired,
refine: PropTypes.func.isRequired,
transformItems: PropTypes.func,
};

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class HierarchicalMenu extends Component {
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
transformItems: PropTypes.func,
};

renderItem = item => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-instantsearch/src/components/HitsPerPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class HitsPerPage extends Component {
static propTypes = {
refine: PropTypes.func.isRequired,
currentRefinement: PropTypes.number.isRequired,

transformItems: PropTypes.func,
items: PropTypes.arrayOf(
PropTypes.shape({
/**
Expand Down
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Menu extends Component {
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
transformItems: PropTypes.func,
};

renderItem = item => {
Expand Down
1 change: 0 additions & 1 deletion packages/react-instantsearch/src/components/Menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import {mount} from 'enzyme';

import Menu from './Menu';

describe('Menu', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/MultiRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class MultiRange extends Component {
value: PropTypes.string.isRequired,
})).isRequired,
refine: PropTypes.func.isRequired,
transformItems: PropTypes.func,
};

renderItem = item => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class RefinementList extends Component {
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
transformItems: PropTypes.func,
};

selectItem = item => {
Expand Down
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/SortBy.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class SortBy extends Component {
})).isRequired,

currentRefinement: PropTypes.string.isRequired,
transformItems: PropTypes.func,
};

onChange = e => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import createConnector from '../core/createConnector';

import {PropTypes} from 'react';
/**
* connectCurrentRefinements connector provides the logic to build a widget that will
* give the user the ability to remove all or some of the filters that were
* set.
* @name connectCurrentRefinements
* @kind connector
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to remove a single filter
* @providedPropType {array.<{label: string, attributeName: string, currentRefinement: string || object, items: array, value: function}>} items - all the filters, the `value` is to pass to the `refine` function for removing all currentrefinements, `label` is for the display. When existing several refinements for the same atribute name, then you get a nested `items` object that contains a `label` and a `value` function to use to remove a single filter. `attributeName` and `currentRefinement` are metadata containing row values.
*/
export default createConnector({
displayName: 'AlgoliaCurrentRefinements',

propTypes: {
transformItems: PropTypes.func,
},

getProvidedProps(props, searchState, searchResults, metadata) {
return {
items: metadata.reduce((res, meta) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const sortBy = ['name:asc'];
* @propType {string} [separator='>'] - Specifies the level separator used in the data.
* @propType {string[]} [rootPath=null] - The already selected and hidden path.
* @propType {boolean} [showParentLevel=true] - Flag to set if the parent level should be displayed.
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to toggle a refinement
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied
Expand All @@ -104,14 +105,11 @@ export default createConnector({
separator: PropTypes.string,
rootPath: PropTypes.string,
showParentLevel: PropTypes.bool,

defaultRefinement: PropTypes.string,

showMore: PropTypes.bool,

limitMin: PropTypes.number,

limitMax: PropTypes.number,
transformItems: PropTypes.func,
},

defaultProps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function getCurrentRefinement(props, searchState) {
* @kind connector
* @propType {number} defaultRefinement - The number of items selected by default
* @propType {{value: number, label: string}[]} items - List of hits per page options.
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to remove a single filter
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied
Expand All @@ -38,6 +39,7 @@ export default createConnector({
label: PropTypes.string,
value: PropTypes.number.isRequired,
})).isRequired,
transformItems: PropTypes.func,
},

getProvidedProps(props, searchState) {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-instantsearch/src/connectors/connectMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const sortBy = ['count:desc', 'name:asc'];
* @propType {number} [limitMin=10] - the minimum number of diplayed items
* @propType {number} [limitMax=20] - the maximun number of displayed items. Only used when showMore is set to `true`
* @propType {string} defaultRefinement - the value of the item selected by default
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to toggle a refinement
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied
Expand All @@ -54,6 +55,7 @@ export default createConnector({
limitMin: PropTypes.number,
limitMax: PropTypes.number,
defaultRefinement: PropTypes.string,
transformItems: PropTypes.func,
},

defaultProps: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function getCurrentRefinement(props, searchState) {
* @propType {string} attributeName - the name of the attribute in the records
* @propType {{label: string, start: number, end: number}[]} items - List of options. With a text label, and upper and lower bounds.
* @propType {string} defaultRefinement - the value of the item selected by default, follow the shape of a `string` with a pattern of `'{start}:{end}'`.
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to select a range.
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied. follow the shape of a `string` with a pattern of `'{start}:{end}'` which corresponds to the current selected item. For instance, when the selected item is `{start: 10, end: 20}`, the searchState of the widget is `'10:20'`. When `start` isn't defined, the searchState of the widget is `':{end}'`, and the same way around when `end` isn't defined. However, when neither `start` nor `end` are defined, the searchState is an empty string.
Expand All @@ -63,6 +64,7 @@ export default createConnector({
start: PropTypes.number,
end: PropTypes.number,
})).isRequired,
transformItems: PropTypes.func,
},

getProvidedProps(props, searchState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ function getValue(name, props, searchState) {
* @propType {string} [operator=or] - How to apply the refinements. Possible values: 'or' or 'and'.
* @propType {string} attributeName - the name of the attribute in the record
* @propType {boolean} [showMore=false] - true if the component should display a button that will expand the number of items
* @propType {number} [limitMin=10] - the minimum number of diplayed items
* @propType {number} [limitMin=10] - the minimum number of displayed items
* @propType {number} [limitMax=20] - the maximun number of displayed items. Only used when showMore is set to `true`
* @propType {string[]} defaultRefinement - the values of the items selected by default. The searchState of this widget takes the form of a list of `string`s, which correspond to the values of all selected refinements. However, when there are no refinements selected, the value of the searchState is an empty string.
* @propType {boolean} [searchForFacetValues=false] - if set to true, the searchForFacetValues function is provided
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to toggle a refinement
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {function} searchForFacetValues - a function to toggle a search for facet values
Expand All @@ -72,6 +73,7 @@ export default createConnector({
limitMax: PropTypes.number,
defaultRefinement: PropTypes.arrayOf(PropTypes.string),
searchForFacetValues: PropTypes.bool,
transformItems: PropTypes.func,
},

defaultProps: {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-instantsearch/src/connectors/connectSortBy.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function getCurrentRefinement(props, searchState) {
* @kind connector
* @propType {string} defaultRefinement - The default selected index.
* @propType {{value, label}[]} items - The list of indexes to search in.
* @propType {function} [transformItems] - If provided, this function can be used to modify the `items` provided prop of the wrapped component (ex: for filtering or sorting items). this function takes the `items` prop as a parameter and expects it back in return.
* @providedPropType {function} refine - a function to remove a single filter
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string[]} currentRefinement - the refinement currently applied
Expand All @@ -39,6 +40,7 @@ export default createConnector({
label: PropTypes.string,
value: PropTypes.string.isRequired,
})).isRequired,
transformItems: PropTypes.func,
},

getProvidedProps(props, searchState) {
Expand Down
13 changes: 10 additions & 3 deletions packages/react-instantsearch/src/core/createConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {shallowEqual, getDisplayName} from './utils';
* @property {function} getMetadata - metadata of the widget
* @property {function} transitionState - hook after the state has changed
* @property {function} getProvidedProps - transform the state into props passed to the wrapped component.
* @property {function} getTransformedItems - apply any user modifications into the `items` prop passed to the wrapped component.
* Receives (props, widgetStates, searchState, metadata) and returns the local state.
* @property {function} getId - Receives props and return the id that will be used to identify the widget
* @property {function} cleanUp - hook when the widget will unmount. Receives (props, searchState) and return a cleaned state.
Expand Down Expand Up @@ -59,12 +60,12 @@ export default function createConnector(connectorDesc) {

const {ais: {store, widgetsManager}} = context;
this.state = {
props: this.getProvidedProps(props),
props: this.getTransformedItems(props),
};

this.unsubscribe = store.subscribe(() => {
this.setState({
props: this.getProvidedProps(this.props),
props: this.getTransformedItems(props),
});
});

Expand Down Expand Up @@ -99,7 +100,7 @@ export default function createConnector(connectorDesc) {
componentWillReceiveProps(nextProps) {
if (!shallowEqual(this.props, nextProps)) {
this.setState({
props: this.getProvidedProps(nextProps),
props: this.getTransformedItems(nextProps),
});

if (isWidget) {
Expand Down Expand Up @@ -181,6 +182,12 @@ export default function createConnector(connectorDesc) {

cleanUp = (...args) => connectorDesc.cleanUp(...args);

getTransformedItems = props => {
const providedProps = this.getProvidedProps(props);
return props.transformItems && providedProps && providedProps.items
? {...providedProps, items: props.transformItems(providedProps.items)} : providedProps;
};

render() {
if (this.state.props === null) {
return null;
Expand Down
Loading

0 comments on commit ba539f0

Please sign in to comment.