Skip to content

Commit

Permalink
feat(connector): Numeric selector
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Stanislawski committed Feb 15, 2017
1 parent 918d971 commit 0dc42d2
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 99 deletions.
6 changes: 3 additions & 3 deletions dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,23 @@ <h1><a href="./">Instant search demo</a> <small>using instantsearch.js</small></
<div class="col-md-3 text-right">
<div class="form-inline">
<div class="form-group">
<label for="hits-per-page-select">Show:</label>
<label for="hits-per-page-select">Hits per page selector / Show:</label>
<span id="hits-per-page-selector"></span>
</div>
</div>
</div>
<div class="col-md-3 text-right">
<div class="form-inline">
<div class="form-group">
<label for="sort-by-selector-select">Popularity:</label>
<label for="sort-by-selector-select">Numeric selector / Popularity:</label>
<span id="popularity-selector"></span>
</div>
</div>
</div>
<div class="col-md-3 text-right">
<div class="form-inline">
<div class="form-group">
<label for="sort-by-selector-select">Sort by:</label>
<label for="sort-by-selector-select">Sort by selector / Sort by:</label>
<span id="sort-by-selector"></span>
</div>
</div>
Expand Down
107 changes: 107 additions & 0 deletions src/connectors/numeric-selector/connectNumericSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
bemHelper,
getContainerNode,
} from '../../lib/utils.js';
import cx from 'classnames';

const bem = bemHelper('ais-numeric-selector');

/**
* Instantiate a dropdown element to choose the number of hits to display per page
* @function numericSelector
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {string} options.attributeName Name of the numeric attribute to use
* @param {Array} options.options Array of objects defining the different values and labels
* @param {number} options.options[i].value The numerical value to refine with
* @param {string} options.options[i].label Label to display in the option
* @param {string} [options.operator='='] The operator to use to refine
* @param {boolean} [options.autoHideContainer=false] Hide the container when no results match
* @param {Object} [options.cssClasses] CSS classes to be added
* @param {string|string[]} [options.cssClasses.root] CSS classes added to the parent `<select>`
* @param {string|string[]} [options.cssClasses.item] CSS classes added to each `<option>`
* @return {Object}
*/
const usage = `Usage: numericSelector({
container,
attributeName,
options,
cssClasses.{root,item},
autoHideContainer
})`;

const connectNumericSelector = numericSelectorRendering => ({
container,
operator = '=',
attributeName,
options,
cssClasses: userCssClasses = {},
autoHideContainer = false,
}) => {
const containerNode = getContainerNode(container);
if (!container || !options || options.length === 0 || !attributeName) {
throw new Error(usage);
}

const cssClasses = {
root: cx(bem(null), userCssClasses.root),
item: cx(bem('item'), userCssClasses.item),
};

return {
getConfiguration(currentSearchParameters, searchParametersFromUrl) {
return {
numericRefinements: {
[attributeName]: {
[operator]: [this._getRefinedValue(searchParametersFromUrl)],
},
},
};
},
init({helper}) {
this._refine = value => {
helper.clearRefinements(attributeName);
if (value !== undefined) {
helper.addNumericRefinement(attributeName, operator, value);
}
helper.search();
};

numericSelectorRendering({
cssClasses,
currentValue: this._getRefinedValue(helper.state),
options,
setValue: this._refine,
shouldAutoHideContainer: autoHideContainer,
containerNode,
}, true);
},

render({helper, results}) {
numericSelectorRendering({
cssClasses,
currentValue: this._getRefinedValue(helper.state),
options,
setValue: this._refine,
shouldAutoHideContainer: autoHideContainer && results.nbHits === 0,
containerNode,
}, false);
},

_getRefinedValue(state) {
// This is reimplementing state.getNumericRefinement
// But searchParametersFromUrl is not an actual SearchParameters object
// It's only the object structure without the methods, because getStateFromQueryString
// is not sending a SearchParameters. There's no way given how we built the helper
// to initialize a true partial state where only the refinements are present
return state &&
state.numericRefinements &&
state.numericRefinements[attributeName] !== undefined &&
state.numericRefinements[attributeName][operator] !== undefined &&
state.numericRefinements[attributeName][operator][0] !== undefined ? // could be 0
state.numericRefinements[attributeName][operator][0] :
options[0].value;
},
};
};

export default connectNumericSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@ describe('numericSelector()', () => {
let expectedProps;
let helper;
let results;
let autoHideContainer;

beforeEach(() => {
autoHideContainer = sinon.stub().returns(Selector);
ReactDOM = {render: sinon.spy()};

numericSelector.__Rewire__('ReactDOM', ReactDOM);
numericSelector.__Rewire__('autoHideContainerHOC', autoHideContainer);

container = document.createElement('div');
options = [
Expand All @@ -44,7 +41,6 @@ describe('numericSelector()', () => {
item: 'ais-numeric-selector--item custom-item',
},
currentValue: 1,
shouldAutoHideContainer: true,
options: [
{value: 1, label: 'first'},
{value: 2, label: 'second'},
Expand Down Expand Up @@ -130,6 +126,5 @@ describe('numericSelector()', () => {

afterEach(() => {
numericSelector.__ResetDependency__('ReactDOM');
numericSelector.__ResetDependency__('autoHideContainerHOC');
});
});
115 changes: 24 additions & 91 deletions src/widgets/numeric-selector/numeric-selector.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {
bemHelper,
getContainerNode,
} from '../../lib/utils.js';
import cx from 'classnames';
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
import SelectorComponent from '../../components/Selector.js';

const bem = bemHelper('ais-numeric-selector');
import Selector from '../../components/Selector.js';
import connectNumericSelector from '../../connectors/numeric-selector/connectNumericSelector.js';

/**
* Instantiate a dropdown element to choose the number of hits to display per page
Expand All @@ -25,86 +18,26 @@ const bem = bemHelper('ais-numeric-selector');
* @param {string|string[]} [options.cssClasses.item] CSS classes added to each `<option>`
* @return {Object}
*/

function numericSelector({
container,
operator = '=',
attributeName,
options,
cssClasses: userCssClasses = {},
autoHideContainer = false,
}) {
const containerNode = getContainerNode(container);
const usage = `Usage: numericSelector({
container,
attributeName,
options,
cssClasses.{root,item},
autoHideContainer
})`;

let Selector = SelectorComponent;
if (autoHideContainer === true) {
Selector = autoHideContainerHOC(Selector);
}

if (!container || !options || options.length === 0 || !attributeName) {
throw new Error(usage);
}

const cssClasses = {
root: cx(bem(null), userCssClasses.root),
item: cx(bem('item'), userCssClasses.item),
};

return {
getConfiguration(currentSearchParameters, searchParametersFromUrl) {
return {
numericRefinements: {
[attributeName]: {
[operator]: [this._getRefinedValue(searchParametersFromUrl)],
},
},
};
},
init({helper}) {
this._refine = value => {
helper.clearRefinements(attributeName);
if (value !== undefined) {
helper.addNumericRefinement(attributeName, operator, value);
}
helper.search();
};
},

render({helper, results}) {
ReactDOM.render(
<Selector
cssClasses={cssClasses}
currentValue={this._getRefinedValue(helper.state)}
options={options}
setValue={this._refine}
shouldAutoHideContainer={results.nbHits === 0}
/>,
containerNode
);
},

_getRefinedValue(state) {
// This is reimplementing state.getNumericRefinement
// But searchParametersFromUrl is not an actual SearchParameters object
// It's only the object structure without the methods, because getStateFromQueryString
// is not sending a SearchParameters. There's no way given how web built the helper
// to initialize a true partial state where only the refinements are present
return state &&
state.numericRefinements &&
state.numericRefinements[attributeName] !== undefined &&
state.numericRefinements[attributeName][operator] !== undefined &&
state.numericRefinements[attributeName][operator][0] !== undefined ? // could be 0
state.numericRefinements[attributeName][operator][0] :
options[0].value;
},
};
export default connectNumericSelector(defaultRendering);

function defaultRendering({
cssClasses,
currentValue,
options,
setValue,
shouldAutoHideContainer,
containerNode,
}, isFirstRendering) {
if (isFirstRendering) return;

ReactDOM.render(
<Selector
cssClasses={cssClasses}
currentValue={currentValue}
options={options}
setValue={setValue}
shouldAutoHideContainer={shouldAutoHideContainer}
/>,
containerNode
);
}

export default numericSelector;

0 comments on commit 0dc42d2

Please sign in to comment.