Skip to content

Commit

Permalink
feat(refinementlist): lets configure showmore feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Stanislawski committed Jan 19, 2016
1 parent c4bf8ec commit 3b8688a
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 27 deletions.
25 changes: 25 additions & 0 deletions dev/app.js
Expand Up @@ -114,6 +114,31 @@ search.addWidget(
container: '#brands',
attributeName: 'brand',
operator: 'or',
limit: 3,
cssClasses: {
header: 'facet-title',
item: 'facet-value checkbox',
count: 'facet-count pull-right',
active: 'facet-active'
},
templates: {
header: 'Brands'
},
showMore: {
templates: {
'active': '<button>Show less</button>',
'inactive': '<button>Show more</button>'
},
limit: 10
}
})
);

search.addWidget(
instantsearch.widgets.refinementList({
container: '#brands-2',
attributeName: 'brand',
operator: 'or',
limit: 10,
cssClasses: {
header: 'facet-title',
Expand Down
1 change: 1 addition & 0 deletions dev/index.html
Expand Up @@ -24,6 +24,7 @@ <h1><a href="./">Instant search demo</a> <small>using instantsearch.js</small></
<div class="facet" id="current-refined-values"></div>
<div class="facet" id="hierarchical-categories"></div>
<div class="facet" id="brands"></div>
<div class="facet" id="brands-2"></div>
<div class="facet" id="price-range"></div>
<div class="facet" id="free-shipping"></div>
<div class="facet" id="price"></div>
Expand Down
4 changes: 4 additions & 0 deletions dev/style.css
Expand Up @@ -118,3 +118,7 @@ body {
.ais-refinement-list--label input[type=radio]{
margin: 4px 8px 0 0;
}

.ais-showmore__active, .ais-showmore__inactive {
cursor: pointer;
}
32 changes: 29 additions & 3 deletions src/components/RefinementList/RefinementList.js
Expand Up @@ -5,6 +5,13 @@ import {isSpecialClick} from '../../lib/utils.js';
import Template from '../Template.js';

class RefinementList extends React.Component {
constructor(props) {
super(props);
this.state = {
isShowMoreOpen: false
};
}

refine(value) {
this.props.toggleRefinement(value);
}
Expand Down Expand Up @@ -45,7 +52,7 @@ class RefinementList extends React.Component {
<div
className={cssClassItem}
key={key}
onClick={this.handleClick.bind(this, facetValue[this.props.attributeNameKey])}
onClick={this.handleItemClick.bind(this, facetValue[this.props.attributeNameKey])}
>
<Template data={templateData} templateKey="item" {...this.props.templateProps} />
{subList}
Expand All @@ -69,7 +76,7 @@ class RefinementList extends React.Component {
//
// Finally, we always stop propagation of the event to avoid multiple levels RefinementLists to fail: click
// on child would click on parent also
handleClick(value, e) {
handleItemClick(value, e) {
if (isSpecialClick(e)) {
// do not alter the default browser behavior
// if one special key is down
Expand Down Expand Up @@ -101,16 +108,32 @@ class RefinementList extends React.Component {
this.refine(value);
}

handleClickShowMore() {
const isShowMoreOpen = !this.state.isShowMoreOpen;
this.setState({isShowMoreOpen});
}

render() {
// Adding `-lvl0` classes
let cssClassList = [this.props.cssClasses.list];
if (this.props.cssClasses.depth) {
cssClassList.push(`${this.props.cssClasses.depth}${this.props.depth}`);
}

const limit = this.state.isShowMoreOpen ? this.props.limitMax : this.props.limitMin;
const showmoreBtn =
this.props.showMore ?
<Template
onClick={() => this.handleClickShowMore()}
templateKey={'showmore-' + (this.state.isShowMoreOpen ? 'active' : 'inactive')}
{...this.props.templateProps}
/> :
undefined;

return (
<div className={cx(cssClassList)}>
{this.props.facetValues.map(this._generateFacetItem, this)}
{this.props.facetValues.map(this._generateFacetItem, this).slice(0, limit)}
{showmoreBtn}
</div>
);
}
Expand All @@ -128,6 +151,9 @@ RefinementList.propTypes = {
}),
depth: React.PropTypes.number,
facetValues: React.PropTypes.array,
limitMax: React.PropTypes.number,
limitMin: React.PropTypes.number,
showMore: React.PropTypes.bool,
templateProps: React.PropTypes.object.isRequired,
toggleRefinement: React.PropTypes.func.isRequired
};
Expand Down
27 changes: 23 additions & 4 deletions src/components/RefinementList/__tests__/RefinementList-test.js
Expand Up @@ -56,8 +56,8 @@ describe('RefinementList', () => {
</div>
</div>
);
expect(out.props.children[0].key).toEqual('facet1');
expect(out.props.children[1].key).toEqual('facet2');
expect(out.props.children[0][0].key).toEqual('facet1');
expect(out.props.children[0][1].key).toEqual('facet2');
});

it('should render default list highlighted', () => {
Expand All @@ -75,7 +75,26 @@ describe('RefinementList', () => {
</div>
</div>
);
expect(out.props.children[0].key).toEqual('facet1/true/42');
expect(out.props.children[0][0].key).toEqual('facet1/true/42');
});

context('showmore', () => {
it('should display the number accordingly to the state : closed', () => {
const out = render({
facetValues: [
{name: 'facet1', isRefined: false, count: 42},
{name: 'facet2', isRefined: false, count: 42}
],
showMore: true,
limitMin: 1,
limitMax: 2
});
expect(out.props.children.length).toBe(2);
});

it('should display the number accordingly to the state : open', () => {
// FIXME find a way to test this state...
});
});

context('sublist', () => {
Expand Down Expand Up @@ -140,7 +159,7 @@ describe('RefinementList', () => {

function render(extraProps = {}) {
let props = getProps(extraProps);
renderer.render(<RefinementList {...props} templateProps={{}} />);
renderer.render(<RefinementList {...props} ref="list" templateProps={{}} />);
return renderer.getRenderOutput();
}

Expand Down
20 changes: 12 additions & 8 deletions src/components/Template.js
Expand Up @@ -13,7 +13,8 @@ function Template(props) {
const compileOptions = useCustomCompileOptions ? props.templatesConfig.compileOptions : {};

const content = renderTemplate({
template: props.templates[props.templateKey],
templates: props.templates,
templateKey: props.templateKey,
compileOptions: compileOptions,
helpers: props.templatesConfig.helpers,
data: transformData(props.transformData, props.templateKey, props.data)
Expand Down Expand Up @@ -79,9 +80,10 @@ function transformData(fn, templateKey, originalData) {
let clonedData = cloneDeep(originalData);

let data;
if (typeof fn === 'function') {
const typeFn = typeof fn;
if (typeFn === 'function') {
data = fn(clonedData);
} else if (typeof fn === 'object') {
} else if (typeFn === 'object') {
// ex: transformData: {hit, empty}
if (fn[templateKey]) {
data = fn[templateKey](clonedData);
Expand All @@ -91,7 +93,7 @@ function transformData(fn, templateKey, originalData) {
data = originalData;
}
} else {
throw new Error('`transformData` must be a function or an object');
throw new Error(`transformData must be a function or an object, was ${typeFn} (key : ${templateKey})`);
}

let dataType = typeof data;
Expand All @@ -102,12 +104,14 @@ function transformData(fn, templateKey, originalData) {
return data;
}

function renderTemplate({template, compileOptions, helpers, data}) {
let isTemplateString = typeof template === 'string';
let isTemplateFunction = typeof template === 'function';
function renderTemplate({templates, templateKey, compileOptions, helpers, data}) {
const template = templates[templateKey];
const templateType = typeof template;
const isTemplateString = templateType === 'string';
const isTemplateFunction = templateType === 'function';

if (!isTemplateString && !isTemplateFunction) {
throw new Error('Template must be `string` or `function`');
throw new Error(`Template must be 'string' or 'function', was '${templateType}' (key: ${templateKey})`);
} else if (isTemplateFunction) {
return template(data);
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/components/__tests__/Template-test.js
Expand Up @@ -10,7 +10,7 @@ expect.extend(expectJSX);

let {createRenderer} = TestUtils;

describe.only('Template', () => {
describe('Template', () => {
let renderer;

beforeEach(() => {
Expand Down
10 changes: 10 additions & 0 deletions src/css/default/_refinement-list.scss
Expand Up @@ -30,3 +30,13 @@
/* widget footer */
}
}

/* Sub block for the show more of the refinement list */
@include block(showmore) {
@include modifier(active) {
/* Show more button is activated */
}
@include modifier(inactive) {
/* Show more button is deactivated */
}
}
2 changes: 1 addition & 1 deletion src/lib/utils.js
Expand Up @@ -114,7 +114,7 @@ function prepareTemplateProps({
};
}

function prepareTemplates(defaultTemplates = [], templates = []) {
function prepareTemplates(defaultTemplates = {}, templates = {}) {
const allKeys = uniq([...(keys(defaultTemplates)), ...(keys(templates))]);

return reduce(allKeys, (config, key) => {
Expand Down
21 changes: 21 additions & 0 deletions src/widgets/refinement-list/__tests__/refinement-list-test.js
Expand Up @@ -292,4 +292,25 @@ describe('refinementList()', () => {
// Then
});
});

context('show more', () => {
it('should return a configuration with the highest limit value (default value)', () => {
const opts = {container, attributeName: 'attributeName', limit: 1, showMore: {}};
const wdgt = refinementList(opts);
const partialConfig = wdgt.getConfiguration({});
expect(partialConfig.maxValuesPerFacet).toBe(100);
});

it('should return a configuration with the highest limit value (custom value)', () => {
const opts = {container, attributeName: 'attributeName', limit: 1, showMore: {limit: 99}};
const wdgt = refinementList(opts);
const partialConfig = wdgt.getConfiguration({});
expect(partialConfig.maxValuesPerFacet).toBe(opts.showMore.limit);
});

it('should not accept a show more limit that is < limit', () => {
const opts = {container, attributeName: 'attributeName', limit: 100, showMore: {limit: 1}};
expect(() => refinementList(opts)).toThrow();
});
});
});
4 changes: 4 additions & 0 deletions src/widgets/refinement-list/defaultShowMoreTemplates.js
@@ -0,0 +1,4 @@
export default {
active: '<a class="ais-showmore ais-showmore__active">Show less</a>',
inactive: '<a class="ais-showmore ais-showmore__inactive">Show more</a>'
};

0 comments on commit 3b8688a

Please sign in to comment.