Skip to content

Commit

Permalink
feat(Menu, connectMenu): add search for facet values (#1822)
Browse files Browse the repository at this point in the history
  • Loading branch information
mthuret authored and vvo committed Jan 16, 2017
1 parent 486f005 commit a6c513e
Show file tree
Hide file tree
Showing 30 changed files with 1,047 additions and 517 deletions.
26 changes: 18 additions & 8 deletions docgen/src/guide/Search_for_facet_values.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,36 @@ 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
If you use the [`<RefinementList/>`](widgets/RefinementList.html), [`<Menu/>`](widgets/Menu.html) widgets
or the [`connectRefinementList`](connectors/connectRefinementList.html), [`<connectMenu/>`](widgets/Menu.html)
connectors then the end user can choose 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.
To activate the search for facet values when using the [`<RefinementList/>`](widgets/RefinementList.html) or the [`<Menu/>`](widgets/Menu.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.
If activated, the widget should display an input to search for facet values.

### RefinementList
<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>

```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>
### Menu
<a class="btn" href="https://community.algolia.com/instantsearch.js/react/storybook/?selectedKind=Menu&selectedStory=with%20search%20for%20facets%20value" target="_blank">View in Storybook</a>

```javascript
<Menu attributeName="category" searchForFacetValues/>
```

## with connectors

When using the [`connectRefinementList`](connectors/connectRefinementList.html) connector, you have two provided props related to the search
for facet values behavior:
When using the [`connectRefinementList`](connectors/connectRefinementList.html) or the [`connectMenu`](connectors/connectMenu.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
Expand Down Expand Up @@ -60,6 +69,7 @@ const RefinementListWithSFFV = connectRefinementList(props => {

<RefinementListWithSFFV attributeName="attributeName" searchForFacetValues/>
```
The concept is identical when using the `connectMenu` connector.

<div class="guide-nav">
<div class="guide-nav-left">
Expand Down
21 changes: 20 additions & 1 deletion packages/react-instantsearch-theme-algolia/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,31 @@
}
}


// ------------------------------------
// Variables
// ------------------------------------
$main-color: #3369e7;

$sffv-searchbox-config:(
input-width: 300px,
input-height: 32px,
border-width: 1px,
border-radius: 3px,
input-border-color: #D4D8E3,
input-focus-border-color: #D4D8E3,
input-background: #FFFFFF,
input-focus-background: #FFFFFF,
font-size: 14px,
placeholder-color: #697782,
icon: sbx-icon-search-13,
icon-size: 14px,
icon-position: left,
icon-color: #bfc7d8,
icon-background: #FFFFFF,
icon-background-opacity: 0,
icon-clear: sbx-icon-clear-3,
icon-clear-size: 12px
);

// ------------------------------------
// Import
Expand Down
11 changes: 11 additions & 0 deletions packages/react-instantsearch-theme-algolia/styles/_Menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
min-height: 22px;
line-height: 22px;
}
.ais-Menu__item a {
outline: 0;
}

.ais-Menu__itemSelected {
font-weight: bold;
Expand Down Expand Up @@ -51,3 +54,11 @@

.ais-Menu__itemLabelSelected{
}

.ais-Menu__SearchBox {
margin-bottom: 3px;
}

.ais-Menu__SearchBox .ais-SearchBox {
@include searchbox($sffv-searchbox-config...);
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,27 +66,6 @@
.ais-RefinementList__itemCheckboxSelected{
}

$sffv-searchbox-config:(
input-width: 300px,
input-height: 32px,
border-width: 1px,
border-radius: 3px,
input-border-color: #D4D8E3,
input-focus-border-color: #D4D8E3,
input-background: #FFFFFF,
input-focus-background: #FFFFFF,
font-size: 14px,
placeholder-color: #697782,
icon: sbx-icon-search-13,
icon-size: 14px,
icon-position: left,
icon-color: #bfc7d8,
icon-background: #FFFFFF,
icon-background-opacity: 0,
icon-clear: sbx-icon-clear-3,
icon-clear-size: 12px
);

.ais-RefinementList__SearchBox {
margin-bottom: 3px;
}
Expand Down
39 changes: 35 additions & 4 deletions packages/react-instantsearch/src/components/List.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {PropTypes, Component} from 'react';
import SearchBox from '../components/SearchBox';

const itemsPropType = PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.any,
Expand All @@ -13,11 +14,14 @@ class List extends Component {
translate: PropTypes.func,
items: itemsPropType,
renderItem: PropTypes.func.isRequired,
selectItem: PropTypes.func,
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
limit: PropTypes.number,
show: PropTypes.func,
searchForFacetValues: PropTypes.func,
isFromSearch: PropTypes.bool.isRequired,
};

constructor() {
Expand Down Expand Up @@ -74,26 +78,53 @@ class List extends Component {

return (
<button disabled={disabled}
{...cx('showMore', disabled && 'showMoreDisabled')}
onClick={this.onShowMoreClick}
{...cx('showMore', disabled && 'showMoreDisabled')}
onClick={this.onShowMoreClick}
>
{translate('showMore', extended)}
</button>
);
}

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

render() {
const {cx, items} = this.props;
const {cx, items, searchForFacetValues} = this.props;
if (items.length === 0) {
return null;
return <div {...cx('root')}>
{this.renderSearchBox()}
</div>;
}

// Always limit the number of items we show on screen, since the actual
// number of retrieved items might vary with the `maxValuesPerFacet` config
// option.
const limit = this.getLimit();
const searchBox = searchForFacetValues ? this.renderSearchBox() : null;
return (
<div {...cx('root')}>
{searchBox}
<div {...cx('items')}>
{items.slice(0, limit).map(item => this.renderItem(item))}
</div>
Expand Down
16 changes: 15 additions & 1 deletion packages/react-instantsearch/src/components/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {pick} from 'lodash';
import translatable from '../core/translatable';
import List from './List';
import Link from './Link';
import Highlight from '../widgets/Highlight';
import classNames from './classNames.js';

const cx = classNames('Menu');
Expand All @@ -11,13 +12,15 @@ class Menu extends Component {
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.string.isRequired,
count: PropTypes.number.isRequired,
isRefined: PropTypes.bool.isRequired,
})),
isFromSearch: PropTypes.bool.isRequired,
showMore: PropTypes.bool,
limitMin: PropTypes.number,
limitMax: PropTypes.number,
Expand All @@ -26,14 +29,17 @@ class Menu extends Component {

renderItem = item => {
const {refine, createURL} = this.props;
const label = this.props.isFromSearch
? <Highlight attributeName="label" hit={item}/>
: item.label;
return (
<Link
{...cx('itemLink', item.isRefined && 'itemLinkSelected')}
onClick={() => refine(item.value)}
href={createURL(item.value)}
>
<span {...cx('itemLabel', item.isRefined && 'itemLabelSelected')}>
{item.label}
{label}
</span>
{' '}
<span {...cx('itemCount', item.isRefined && 'itemCountSelected')}>
Expand All @@ -43,17 +49,24 @@ class Menu extends Component {
);
};

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

render() {
return (
<List
renderItem={this.renderItem}
selectItem={this.selectItem}
cx={cx}
{...pick(this.props, [
'translate',
'items',
'showMore',
'limitMin',
'limitMax',
'isFromSearch',
'searchForFacetValues',
])}
/>
);
Expand All @@ -62,4 +75,5 @@ class Menu extends Component {

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

0 comments on commit a6c513e

Please sign in to comment.