Skip to content

Commit

Permalink
feat(RefinementList, connectRefinementList): allow to search for face…
Browse files Browse the repository at this point in the history
…t values

This PR adds search for facet values feature to both RefinementList and connectRefinementList.

We might add it to Menu and connectMenu also soon.
  • Loading branch information
mthuret authored and vvo committed Jan 4, 2017
1 parent b7d25b2 commit e086a81
Show file tree
Hide file tree
Showing 25 changed files with 842 additions and 208 deletions.
6 changes: 2 additions & 4 deletions docgen/layouts/widget.pug
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ block content
div.api-entry(id=themeId)=type.key
a.anchor(href=`${navPath}#${themeId}`)
tr.api-entry-description
td
p=type.description
td(colspan=3)!=h.markdown(type.description)
else
p This widget does not accept theme.
h2#translations Translation keys
Expand All @@ -72,7 +71,6 @@ block content
div.api-entry(id=themeId)=type.key
a.anchor(href=`${navPath}#${themeId}`)
tr.api-entry-description
td
p=type.description
td(colspan=3)!=h.markdown(type.description)
else
p This widget does not have translations.
7 changes: 6 additions & 1 deletion docgen/src/examples/e-commerce-infinite/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,15 @@ const Facets = () =>
<SideBarSection
title="Refine by"
items={[
<RefinementListWithTitle
title="Type"
key="Type"
item={<RefinementList attributeName="type" operator="or" limitMin={5} searchForFacetValues/>}
/>,
<RefinementListWithTitle
title="Materials"
key="Materials"
item={<RefinementList attributeName="materials" operator="or" limitMin={10}/>}
item={<RefinementList attributeName="materials" operator="or" limitMin={5} searchForFacetValues/>}
/>,
<RefinementListWithTitle
title="Color"
Expand Down
11 changes: 8 additions & 3 deletions docgen/src/examples/e-commerce/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,15 @@ const Facets = () =>
<SideBarSection
title="Refine by"
items={[
<RefinementListWithTitle
title="Type"
key="Type"
item={<RefinementList attributeName="type" operator="or" limitMin={5} searchForFacetValues/>}
/>,
<RefinementListWithTitle
title="Materials"
key="Materials"
item={<RefinementList attributeName="materials" operator="or" limitMin={10}/>}
item={<RefinementList attributeName="materials" operator="or" limitMin={5} searchForFacetValues/>}
/>,
<RefinementListWithTitle
title="Color"
Expand Down Expand Up @@ -190,8 +195,8 @@ const Hit = ({item}) => {
<div className="product-picture"><img src={`${item.image}`}/></div>
</div>
<div className="product-desc-wrapper">
<div className="product-name"><Highlight attributeName="name" hit={item} /></div>
<div className="product-type"><Highlight attributeName="type" hit={item} /></div>
<div className="product-name"><Highlight attributeName="name" hit={item}/></div>
<div className="product-type"><Highlight attributeName="type" hit={item}/></div>
<div className="ais-StarRating__ratingLink">
{icons}
<div className="product-price">${item.price}</div>
Expand Down
20 changes: 19 additions & 1 deletion docgen/src/guide/Custom_connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ If you wish to implement features that are not covered by the default widgets co

Those properties are directly applied to the higher-order component. Providing a `displayName` is mandatory.

## getProvidedProps(props, searchState, searchResults, meta)
## getProvidedProps(props, searchState, searchResults, meta, searchForFacetValuesResults)

This method should return the props to forward to the composed component.

Expand All @@ -24,6 +24,8 @@ This method should return the props to forward to the composed component.

`meta` is the list of metadata from all widgets whose connector defines a `getMetadata` method.

`searchForFacetValuesResults` holds the search for facet values results.

## refine(props, searchState, ...args)

This method defines exactly how the `refine` prop of widgets affects the search state.
Expand Down Expand Up @@ -174,6 +176,22 @@ const CoolWidget = createConnector({
})(Widget);
```

## searchForFacetValues(props, searchState, nextRefinement)

This method needs to be implemented if you want to have the ability to perform a search for facet values inside your widget.

It takes in the current props of the higher-order component, the [search state](guide/Search_state.html) of all widgets, as well as all arguments passed to the `searchForFacetValues` props of stateful widgets, and returns an
object of the shape: `{facetName: string, query: string}`

```javascript
const CoolWidget = createConnector({
// displayName, getProvidedProps, refine, getSearchParameters, getMetadata

searchForFacetValues(props, searchState, nextRefinement) {
return {facetName: props.attributeName, query: nextRefinement};
},
})(Widget);
```
## cleanUp(props, searchState)

This method is called when a widget is about to unmount in order to clean the searchState.
Expand Down
5 changes: 3 additions & 2 deletions docgen/src/guide/Default_refinements.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ const App = () =>
```

**Notes:**
* The [search state guide](guide/Search_state.html) details all widgets and connectors state values.._* Default refinements are handy when used as [Virtual widgets](guide/Virtual_widgets.html).
* The [search state guide](guide/Search_state.html) details all widgets and connectors state values...
* Default refinements are handy when used as [Virtual widgets](guide/Virtual_widgets.html).

<div class="guide-nav">
<div class="guide-nav-left">
Previous: <a href="guide/Connectors.html">← Connectors</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Virtual_widgets.html">Virtual Widgets →</a>
Next: <a href="guide/Search_for_facet_values.html">Search for facet values →</a>
</div>
</div>
71 changes: 71 additions & 0 deletions docgen/src/guide/Search_for_facet_values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Search for facet values
mainTitle: Guide
layout: main.pug
category: guide
navWeight: 68
---

If you use the [`<RefinementList/>`](widgets/RefinementList.html) widget or the [`connectRefinementList`](connectors/connectRefinementList.html)
connector then the end user can choose multiple values for a specific facet. If a facet has a lot of possible values then you can decide
to let the end user search inside them before selecting them. This feature is called search for facet values.

## with widgets

To activate the search for facet values when using the [`<RefinementList/>`](widgets/RefinementList.html) widget you need to pass the `searchForFacetValues`
boolean as a prop.

If activated, the refinement list should display an input to search for facet values.

```javascript
<RefinementList attributeName="attributeName" searchForFacetValues/>
```

<a class="btn" href="https://community.algolia.com/instantsearch.js/react/storybook/?selectedKind=RefinementList&selectedStory=with%20search%20for%20facets%20value" target="_blank">View in Storybook</a>

## with connectors

When using the [`connectRefinementList`](connectors/connectRefinementList.html) connector, you have two provided props related to the search
for facet values behavior:

* `isFromSearch`, If `true` this boolean indicate that the `items` prop contains the search for facet values results.
* `searchForFacetValues`, a function to call when triggering the search for facet values. It takes one parameter, the search
for facet values query.

You will also need to pass the `searchForFacetValues` boolean as a prop.

```javascript
import {connectRefinementList} from '../packages/react-instantsearch/connectors';
import {Highlight} from '../packages/react-instantsearch/dom';

const RefinementListWithSFFV = connectRefinementList(props => {
const values = props.items.map(item => {
const label = item._highlightResult
? <Highlight attributeName="label" hit={item}/>
: item.label;

return <li key={item.value}>
<a onClick={() => props.refine(item.value)}>
{label} {item.isRefined ? '- selected' : ''}
</a>
</li>;
});
return (
<div>
<input type="search" onChange={e => props.searchForFacetValues(e.target.value)}/>
<ul>{values}</ul>
</div>
);
});

<RefinementListWithSFFV attributeName="attributeName" searchForFacetValues/>
```

<div class="guide-nav">
<div class="guide-nav-left">
Previous: <a href="guide/Default_refinements.html">← Default refinements</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Virtual_widgets.html">Virtual widgets →</a>
</div>
</div>
2 changes: 1 addition & 1 deletion docgen/src/guide/Virtual_widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const App = () =>

<div class="guide-nav">
<div class="guide-nav-left">
Previous: <a href="guide/Default_refinements.html">← Default Refinements</a>
Previous: <a href="guide/Search_for_facet_values.html">← Search for facet values</a>
</div>
<div class="guide-nav-right">
Next: <a href="guide/Routing.html">Routing →</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,16 @@

.ais-RefinementList__itemCheckboxSelected{
}

.ais-RefinementList__SearchBox input.ais-SearchBox__input[type="search"]{
@extends input.ais-SearchBox__input[type="search"];
background: url("data:image/svg+xml;utf8,<svg viewBox=\'0 0 18 18\' xmlns=\'http://www.w3.org/2000/svg\'><path d=\'M12.5 11h-.79l-.28-.27C12.41 9.59 13 8.11 13 6.5 13 2.91 10.09 0 6.5 0S0 2.91 0 6.5 2.91 13 6.5 13c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L17.49 16l-4.99-5zm-6 0C4.01 11 2 8.99 2 6.5S4.01 2 6.5 2 11 4.01 11 6.5 8.99 11 6.5 11z\' fill=\'%23BFC7D8\' fill-rule=\'evenodd\'/></svg>")no-repeat center left 5px / 18px;
padding: 5px 44px;
margin-bottom: 5px;
}

.ais-RefinementList__SearchBox .ais-SearchBox__reset {
@extends .ais-SearchBox__reset;
top: 0.3em;
right: 0.55em;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
.ais-SearchBox__root {
width: 300px;

* {
// outline: 1px solid rgba(red, 0.3)
}
Expand Down
61 changes: 53 additions & 8 deletions packages/react-instantsearch/src/components/RefinementList.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,64 @@ import {pick} from 'lodash';
import translatable from '../core/translatable';
import List from './List';
import classNames from './classNames.js';

import Highlight from '../widgets/Highlight';
import SearchBox from '../components/SearchBox';
const cx = classNames('RefinementList');

class RefinementList extends Component {
constructor(props) {
super(props);
this.state = {query: ''};
}

static propTypes = {
translate: PropTypes.func.isRequired,
refine: PropTypes.func.isRequired,
searchForFacetValues: PropTypes.func,
createURL: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.string).isRequired,
count: PropTypes.number.isRequired,
isRefined: PropTypes.bool.isRequired,
})),
isFromSearch: PropTypes.bool.isRequired,
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
};

renderItem = item =>
selectItem = item => {
this.props.refine(item.value);
};

renderItem = item => {
const label = item._highlightResult
? <Highlight attributeName="label" hit={item}/>
: item.label;

return (
<label>
<input
{...cx('itemCheckbox', item.isRefined && 'itemCheckboxSelected')}
type="checkbox"
checked={item.isRefined}
onChange={() => this.props.refine(item.value)}
onChange={() => this.selectItem(item)}
/>
<span {...cx('itemBox', 'itemBox', item.isRefined && 'itemBoxSelected')}></span>
<span {...cx('itemLabel', 'itemLabel', item.isRefined && 'itemLabelSelected')}>
{item.label}
{label}
</span>
{' '}
<span {...cx('itemCount', item.isRefined && 'itemCountSelected')}>
{item.count}
</span>
</label>
;
</label>);
};

render() {
return (
<List
const facets = this.props.items.length > 0
? <List
renderItem={this.renderItem}
cx={cx}
{...pick(this.props, [
Expand All @@ -54,10 +71,38 @@ class RefinementList extends Component {
'limitMax',
])}
/>
: <div>{this.props.translate('noResults')}</div>;

const searchBox = this.props.searchForFacetValues ?
<div {...cx('SearchBox')}>
<SearchBox
currentRefinement={this.props.isFromSearch ? this.state.query : ''}
refine={value => {
this.setState({query: value});
this.props.searchForFacetValues(value);
}}
translate={this.props.translate}
onSubmit={e => {
e.preventDefault();
e.stopPropagation();
if (this.props.isFromSearch) {
this.selectItem(this.props.items[0]);
}
}}
/>
</div> : null;

return (
<div>
{searchBox}
{facets}
</div>
);
}
}

export default translatable({
showMore: extended => extended ? 'Show less' : 'Show more',
noResults: 'No Results',
})(RefinementList);

Loading

0 comments on commit e086a81

Please sign in to comment.