Skip to content

Commit

Permalink
feat(menu,refinementList): add header/item/footer templating solution
Browse files Browse the repository at this point in the history
fixes #101

BREAKING CHANGE:

Removed from menu and refinementList:
- rootClass => cssClasses.root
- itemCLass => cssClasses.item
- template => templates.item

Added to menu and refinementList:
- cssClasses{root,list,item}
- templates{header,item,footer}
- widget (container) is automatically hidden by default
- hideWhenNoResults=true

This was done to allow more templating solutions like discussed in #101.
  • Loading branch information
vvo committed Sep 22, 2015
1 parent 827e779 commit 58275dc
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 72 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,20 @@ search.addWidget(
* @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.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} [options.limit=100] How much facet values to get
* @param {Object} [options.cssClasses] Css classes to add to the wrapping elements: root, list, item
* @param {String|String[]} [options.cssClasses.root]
* @param {String|String[]} [options.cssClasses.list]
* @param {String|String[]} [options.cssClasses.item]
* @param {Object} [options.templates] Templates to use for the widget
* @param {String|Function} [options.templates.header] Header template
* @param {String|Function} [options.templates.item=`<label>
<input type="checkbox" value="{{name}}" {{#isRefined}}checked{{/isRefined}} />{{name}} <span>{{count}}</span>
</label>`] Item template, provided with `name`, `count`, `isRefined`
* @param {String|Function} [options.templates.footer] Footer template
* @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.
* to build radio based refinement lists for example
* @param {boolean} [hideWhenNoResults=true] Hide the container when no results match
* @return {Object}
*/
```
Expand Down Expand Up @@ -406,10 +414,16 @@ search.addWidget(
* @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`
* @param {String} [options.limit=100] How much facet values to get
* @param {Object} [options.cssClasses] Css classes to add to the wrapping elements: root, list, item
* @param {String|String[]} [options.cssClasses.root]
* @param {String|String[]} [options.cssClasses.list]
* @param {String|String[]} [options.cssClasses.item]
* @param {Object} [options.templates] Templates to use for the widget
* @param {String|Function} [options.templates.header=''] Header template
* @param {String|Function} [options.templates.item='<a href="{{href}}">{{name}}</a> {{count}}'] Item template, provided with `name`, `count`, `isRefined`
* @param {String|Function} [options.templates.footer=''] Footer template
* @param {boolean} [hideWhenNoResults=true] Hide the container when no results match
* @return {Object}
*/
```
Expand Down Expand Up @@ -472,13 +486,13 @@ search.addWidget(

## Browser support

We support IE9+ and all other modern browsers.
We support IE10+ and all other modern browsers.

To get IE8 support, please insert this in the `<head>`:
To get < IE10 support, please insert this in the `<head>`:

```html
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--[if lte IE 8]>
<!--[if lte IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/aight/1.2.2/aight.min.js"></script>
<![endif]-->
```
Expand Down
64 changes: 48 additions & 16 deletions components/RefinementList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var React = require('react');

var Template = require('./Template');
var cx = require('classnames');

class RefinementList extends React.Component {
refine(value) {
Expand Down Expand Up @@ -38,37 +39,68 @@ class RefinementList extends React.Component {

render() {
var facetValues = this.props.facetValues;
var template = this.props.template;
var templates = this.props.templates;
var rootClass = cx(this.props.cssClasses.root);
var listClass = cx(this.props.cssClasses.list);
var itemClass = cx(this.props.cssClasses.item);

return (
<div className={this.props.rootClass}>
<div className={rootClass}>
<Template template={templates.header} />
<div className={listClass}>
{facetValues.map(facetValue => {
return (
<div className={this.props.itemClass} key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
<Template data={facetValue} template={template} />
<div className={itemClass} key={facetValue.name} onClick={this.handleClick.bind(this, facetValue.name)}>
<Template data={facetValue} template={templates.item} />
</div>
);
})}
</div>
<Template template={templates.footer} />
</div>
);
}
}

RefinementList.propTypes = {
rootClass: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
itemClass: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
cssClasses: React.PropTypes.shape({
root: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
item: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
]),
list: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.arrayOf(React.PropTypes.string)
])
}),
facetValues: React.PropTypes.array,
template: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]).isRequired,
templates: React.PropTypes.shape({
header: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]),
item: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
]).isRequired,
footer: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.func
])
}),
toggleRefinement: React.PropTypes.func.isRequired
};

RefinementList.defaultProps = {
cssClasses: {
root: null,
item: null,
list: null
}
};

module.exports = RefinementList;
4 changes: 4 additions & 0 deletions components/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ Template.propTypes = {
data: React.PropTypes.object
};

Template.defaultProps = {
data: {}
};

module.exports = Template;
26 changes: 21 additions & 5 deletions example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ search.addWidget(
facetName: 'brand',
operator: 'or',
limit: 10,
rootClass: 'nav nav-stacked',
template: require('./templates/or.html')
cssClasses: {
list: 'nav nav-stacked panel-body'
},
templates: {
header: '<div class="panel-heading">Brands</div>',
item: require('./templates/or.html')
}
})
);

Expand All @@ -69,8 +74,13 @@ search.addWidget(
facetName: 'price_range',
operator: 'and',
limit: 10,
rootClass: 'nav nav-stacked',
template: require('./templates/and.html')
cssClasses: {
root: 'list-group'
},
templates: {
header: '<div class="panel-heading">Price ranges</div>',
item: require('./templates/and.html')
}
})
);

Expand All @@ -88,7 +98,13 @@ search.addWidget(
container: '#categories',
facetName: 'categories',
limit: 10,
template: require('./templates/category.html')
cssClasses: {
root: 'list-group'
},
templates: {
header: '<div class="panel-heading">Categories</div>',
item: require('./templates/category.html')
}
})
);

Expand Down
15 changes: 3 additions & 12 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,11 @@ <h1>Instant search demo <small>using instantsearch.js</small></h1>
<div class="panel-body" id="search-box"></div>
</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" id="categories"></div>

<div class="panel panel-default">
<div class="panel-heading">Brands</div>
<div class="panel-body" id="brands"></div>
</div>
<div class="panel panel-default" id="brands"></div>

<div class="panel panel-default">
<div class="panel-heading">Price Ranges</div>
<div class="list-group" id="price_range"></div>
</div>
<div class="panel panel-default" id="price-range"></div>

<div class="panel panel-default">
<div class="panel-heading">Shipping</div>
Expand Down
5 changes: 5 additions & 0 deletions example/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ body {
#price {
padding: 30px 0;
}

.panel-heading {
background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);
border-color: #ddd;
}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ var toFactory = require('to-factory');
var InstantSearch = require('./lib/InstantSearch');
var instantsearch = toFactory(InstantSearch);

require('style?prepend!raw!./lib/style.css');

instantsearch.widgets = {
hits: require('./widgets/hits'),
indexSelector: require('./widgets/index-selector'),
Expand Down
3 changes: 3 additions & 0 deletions lib/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.as-display-none {
display: none;
}
57 changes: 44 additions & 13 deletions widgets/menu.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
var React = require('react');
var cx = require('classnames');

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

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

var defaultTemplates = {
header: '',
footer: '',
item: '<a href="{{href}}">{{name}}</a> {{count}}'
};

var hierarchicalCounter = 0;

/**
* Instantiate a list of refinements based on a facet
* 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`
* @param {String} [options.limit=100] How much facet values to get
* @param {Object} [options.cssClasses] Css classes to add to the wrapping elements: root, list, item
* @param {String|String[]} [options.cssClasses.root]
* @param {String|String[]} [options.cssClasses.list]
* @param {String|String[]} [options.cssClasses.item]
* @param {Object} [options.templates] Templates to use for the widget
* @param {String|Function} [options.templates.header=''] Header template
* @param {String|Function} [options.templates.item='<a href="{{href}}">{{name}}</a> {{count}}'] Item template, provided with `name`, `count`, `isRefined`
* @param {String|Function} [options.templates.footer=''] Footer template
* @param {boolean} [hideWhenNoResults=true] Hide the container when no results match
* @return {Object}
*/
function menu({
container = null,
facetName = null,
sortBy = ['count:desc'],
limit = 100,
rootClass = null,
itemClass = null,
template = defaultTemplate
cssClasses = {
root: null,
list: null,
item: null
},
hideWhenNoResults = true,
templates = defaultTemplates
}) {
hierarchicalCounter++;

Expand All @@ -38,6 +52,10 @@ function menu({
throw new Error(usage);
}

if (templates !== defaultTemplates) {
templates = Object.assign({}, defaultTemplates, templates);
}

var hierarchicalFacetName = 'instantsearch.js' + hierarchicalCounter;

return {
Expand All @@ -48,12 +66,25 @@ function menu({
}]
}),
render: function({results, helper}) {
var values = getFacetValues(results, hierarchicalFacetName, sortBy, limit);

if (values.length === 0) {
React.render(<div/>, containerNode);
if (hideWhenNoResults === true) {
containerNode.classList.add('as-display-none');
}
return;
}

if (hideWhenNoResults === true) {
containerNode.classList.remove('as-display-none');
}

React.render(
<RefinementList
rootClass={cx(rootClass)}
itemClass={cx(itemClass)}
cssClasses={cssClasses}
facetValues={getFacetValues(results, hierarchicalFacetName, sortBy, limit)}
template={template}
templates={templates}
toggleRefinement={toggleRefinement.bind(null, helper, hierarchicalFacetName)}
/>,
containerNode
Expand Down
Loading

0 comments on commit 58275dc

Please sign in to comment.