Skip to content

Commit

Permalink
feat(menu): first widget version
Browse files Browse the repository at this point in the history
  • Loading branch information
vvo committed Sep 7, 2015
1 parent 5cbf451 commit a888143
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 24 deletions.
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,17 @@ search.addWidget(

```js
/**
* Instantiate a list of refinement based on a facet
* Instantiate a list of refinements based on a facet
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
* @param {String} options.facetName Name of the attribute for faceting
* @param {String} options.operator How to apply refinements. Possible values: `or`, `and`
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
* @param {String} [options.limit=100] How much facet values to get.
* @param {String|String[]} [options.cssClass=null] CSS class(es) for the main `<ul>` wrapper.
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
* @param {String|Function} [options.singleRefine=true] Are multiple refinements allowed or only one at the same time. You can use this
* to build radio based refinement lists for example.
* @return {Object}
*/
```
Expand All @@ -199,3 +202,37 @@ search.addWidget(
})
);
```

### menu

#### API

```js
/**
* Create a menu out of a facet
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
* @param {String} options.facetName Name of the attribute for faceting
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
* @param {String} [options.limit=100] How much facet values to get.
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
* @return {Object}
*/
```


#### Usage

```html
<div id="categories"></div>
```

```js
search.addWidget(
instantsearch.widgets.menu({
container: '#categories',
facetName: 'categories'
})
);
```
14 changes: 8 additions & 6 deletions components/MultipleChoiceList.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ var Template = require('./Template');
class MultipleChoiceList extends React.Component {
refine(value) {
this.props.toggleRefine(value);
this.props.search();
}

// Click events on DOM tree like LABEL > INPUT will result in two click events
Expand Down Expand Up @@ -46,10 +45,10 @@ class MultipleChoiceList extends React.Component {
var template = this.props.template;

return (
<ul className={this.props.cssClass}>
<ul className={this.props.rootClass}>
{facetValues.map(facetValue => {
return (
<li key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
<li className={this.props.itemClass} key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
<Template data={facetValue} template={template} />
</li>
);
Expand All @@ -60,12 +59,15 @@ class MultipleChoiceList extends React.Component {
}

MultipleChoiceList.propTypes = {
cssClass: React.PropTypes.oneOfType([
rootClass: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array
React.PropTypes.arrayOf(React.PropTypes.string)
]),
itemClass: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
facetValues: React.PropTypes.array,
search: React.PropTypes.func.isRequired,
template: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
Expand Down
12 changes: 11 additions & 1 deletion example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,19 @@ search.addWidget(
facetName: 'brand',
operator: 'or',
limit: 10,
cssClass: 'nav nav-stacked',
rootClass: 'nav nav-stacked',
template: require('./templates/or.html')
})
);

search.addWidget(
instantsearch.widgets.menu({
container: '#categories',
facetName: 'categories',
limit: 10,
rootClass: 'list-unstyled',
template: require('./templates/category.html')
})
);

search.start();
15 changes: 12 additions & 3 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>

<div class="row">
<div class="col-md-3">
<div id="search-box"></div>
<div class="panel">
<div class="panel-body" id="search-box"></div>
</div>

<h2>Brands</h1>
<div id="brands"></div>
<div class="panel panel-default">
<div class="panel-heading">Categories</div>
<div id="categories" class="list-group"></div>
</div>

<div class="panel panel-default">
<div class="panel-heading">Brands</div>
<div class="panel-body" id="brands"></div>
</div>
</div>
<div class="col-md-9">
<div id="stats"></div>
Expand Down
1 change: 1 addition & 0 deletions example/templates/category.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="#" class="list-group-item{{#isRefined}} active{{/isRefined}}">{{name}} <span class="badge">{{count}}</span></a>
9 changes: 6 additions & 3 deletions example/templates/or.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<label class="checkbox">
<input type="checkbox" value="{{name}}" {{#isRefined}}checked{{/isRefined}} />{{name}} <span class="badge pull-right">{{count}}</span>
</label>
<div class="checkbox">
<label>
<input type="checkbox" value="{{name}}" {{#isRefined}}checked{{/isRefined}} />{{name}}
</label>
<span class="badge pull-right">{{count}}</span>
</div>
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
InstantSearch: require('./lib/InstantSearch'),
widgets: {
hits: require('./widgets/hits'),
menu: require('./widgets/menu'),
multipleChoiceList: require('./widgets/multiple-choice-list'),
pagination: require('./widgets/pagination'),
searchBox: require('./widgets/search-box'),
Expand Down
77 changes: 77 additions & 0 deletions widgets/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
var React = require('react');
var cx = require('classnames');

var utils = require('../lib/widget-utils.js');

var defaultTemplate = `<a href="{{href}}">{{name}}</a> {{count}}`;

var hierarchicalCounter = 0;

/**
* Instantiate a list of refinements based on a facet
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
* @param {String} options.facetName Name of the attribute for faceting
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
* @param {String} [options.limit=100] How much facet values to get.
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
* @return {Object}
*/
function menu({
container = null,
facetName = null,
sortBy = ['count:desc'],
limit = 100,
rootClass = null,
itemClass = null,
template = defaultTemplate
}) {
hierarchicalCounter++;

var MultipleChoiceList = require('../components/MultipleChoiceList');

var containerNode = utils.getContainerNode(container);
var usage = 'Usage: menu({container, facetName, [sortBy, limit, rootClass, itemClass, template]})';

if (container === null || facetName === null) {
throw new Error(usage);
}

var hierarchicalFacetName = 'instantsearch.js' + hierarchicalCounter;

return {
getConfiguration: () => ({
hierarchicalFacets: [{
name: hierarchicalFacetName,
attributes: [facetName]
}]
}),
render: function(results, state, helper) {
React.render(
<MultipleChoiceList
rootClass={cx(rootClass)}
itemClass={cx(itemClass)}
facetValues={getFacetValues(results, hierarchicalFacetName, sortBy, limit)}
template={template}
toggleRefine={toggleRefine.bind(null, helper, hierarchicalFacetName)}
/>,
containerNode
);
}
};
}

function toggleRefine(helper, facetName, facetValue) {
helper
.toggleRefine(facetName, facetValue)
.search();
}

function getFacetValues(results, hierarchicalFacetName, sortBy, limit) {
return results
.getFacetValues(hierarchicalFacetName, {sortBy: sortBy})
.data.slice(0, limit);
}

module.exports = menu;
36 changes: 27 additions & 9 deletions widgets/multiple-choice-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ var defaultTemplate = `<label>
</label>`;

/**
* Instantiate a list of refinement based on a facet
* Instantiate a list of refinements based on a facet
* @param {String|DOMElement} options.container Valid CSS Selector as a string or DOMElement
* @param {String} options.facetName Name of the attribute for faceting
* @param {String} options.operator How to apply refinements. Possible values: `or`, `and`
* @param {String[]} [options.sortBy=['count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|desc`
* @param {String} [options.limit=100] How much facet values to get.
* @param {String|String[]} [options.cssClass=null] CSS class(es) for the main `<ul>` wrapper.
* @param {String|String[]} [options.rootClass=null] CSS class(es) for the root `<ul>` element
* @param {String|String[]} [options.itemClass=null] CSS class(es) for the item `<li>` element
* @param {String|Function} [options.template] Item template, provided with `name`, `count`, `isRefined`
* @param {String|Function} [options.singleRefine=true] Are multiple refinements allowed or only one at the same time. You can use this
* to build radio based refinement lists for example.
* @return {Object}
*/
function multipleChoiceList({
Expand All @@ -24,17 +27,22 @@ function multipleChoiceList({
operator = null,
sortBy = ['count:desc'],
limit = 100,
cssClass = null,
template = defaultTemplate
rootClass = null,
itemClass = null,
template = defaultTemplate,
singleRefine = false
}) {
var MultipleChoiceList = require('../components/MultipleChoiceList');

var containerNode = utils.getContainerNode(container);
var usage = 'Usage: multipleChoiceList({container, facetName, operator[sortBy, limit, cssClass, template]})';
var usage = 'Usage: multipleChoiceList({container, facetName, operator[sortBy, limit, rootClass, itemClass, template]})';

if (container === null ||
facetName === null ||
operator === null) {
// operator is mandatory when multiple refines allowed
(operator === null && singleRefine === false) ||
// operator is not allowed when single refine enabled
(operator !== null && singleRefine === true)) {
throw new Error(usage);
}

Expand All @@ -50,16 +58,26 @@ function multipleChoiceList({
render: function(results, state, helper) {
React.render(
<MultipleChoiceList
cssClass={cx(cssClass)}
rootClass={cx(rootClass)}
itemClass={cx(itemClass)}
facetValues={results.getFacetValues(facetName, {sortBy: sortBy}).slice(0, limit)}
search={helper.search.bind(helper)}
template={template}
toggleRefine={helper.toggleRefine.bind(helper, facetName)}
toggleRefine={toggleRefine.bind(null, helper, singleRefine, facetName)}
/>,
containerNode
);
}
};
}

function toggleRefine(helper, singleRefine, facetName, facetValue) {
if (singleRefine) {
helper.clearRefinement(facetName);
}

helper
.toggleRefine(facetName, facetValue)
.search();
}

module.exports = multipleChoiceList;

0 comments on commit a888143

Please sign in to comment.