@@ -306,13 +306,10 @@ Those two features are implemented respectively with the [pagination](widgets/pa
`,
+ },
+ },
+ },
+ ];
+
+ const escapedHits = [
+ {
+ _highlightResult: {
+ foobar: {
+ value: '<script>
foobar </script>',
+ },
+ },
+ },
+ ];
+
+ escapedHits.__escaped = true;
+
+ widget.init({ helper, instantSearchInstance: {} });
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const rendering = renderFn.mock.calls[1][0];
+
+ expect(rendering.indices[0].hits).toEqual(escapedHits);
+ });
+
+ it('without escapeHTML should not escape the hits', () => {
+ const renderFn = jest.fn();
+ const makeWidget = connectAutocomplete(renderFn);
+ const widget = makeWidget({ escapeHTML: false });
+
+ const helper = jsHelper(fakeClient, '', {});
+ helper.search = jest.fn();
+
+ const hits = [
+ {
+ _highlightResult: {
+ foobar: {
+ value: ``,
+ },
+ },
+ },
+ ];
+
+ widget.init({ helper, instantSearchInstance: {} });
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const rendering = renderFn.mock.calls[1][0];
+
+ expect(rendering.indices[0].hits).toEqual(hits);
+ });
});
diff --git a/src/connectors/autocomplete/connectAutocomplete.js b/src/connectors/autocomplete/connectAutocomplete.js
index 83bdac04d5..3e12c44535 100644
--- a/src/connectors/autocomplete/connectAutocomplete.js
+++ b/src/connectors/autocomplete/connectAutocomplete.js
@@ -1,4 +1,4 @@
-import escapeHits, { tagConfig } from '../../lib/escape-highlight';
+import escapeHits, { TAG_PLACEHOLDER } from '../../lib/escape-highlight';
import { checkRendering } from '../../lib/utils';
const usage = `Usage:
@@ -11,7 +11,7 @@ var customAutcomplete = connectAutocomplete(function render(params, isFirstRende
});
search.addWiget(customAutcomplete({
[ indices ],
- [ escapeHits = false ]
+ [ escapeHTML = true ],
}));
Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectAutocomplete.html
`;
@@ -35,16 +35,13 @@ Full documentation available at https://community.algolia.com/instantsearch.js/c
/**
* @typedef {Object} CustomAutocompleteWidgetOptions
* @property {{value: string, label: string}[]} [indices = []] Name of the others indices to search into.
- * @property {boolean} [escapeHits = false] If true, escape HTML tags from `hits[i]._highlightResult`.
+ * @property {boolean} [escapeHTML = true] If true, escape HTML tags from `hits[i]._highlightResult`.
*/
/**
* **Autocomplete** connector provides the logic to build a widget that will give the user the ability to search into multiple indices.
*
* This connector provides a `refine()` function to search for a query and a `currentRefinement` as the current query used to search.
- *
- * THere's a complete example available on how to write a custom **Autocomplete** widget:
- * [autocomplete.js](https://github.com/algolia/instantsearch.js/blob/develop/dev/app/custom-widgets/jquery/autocomplete.js)
* @type {Connector}
* @param {function(AutocompleteRenderingOptions, boolean)} renderFn Rendering function for the custom **Autocomplete** widget.
* @param {function} unmountFn Unmount function called when the widget is disposed.
@@ -54,7 +51,7 @@ export default function connectAutocomplete(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { indices = [] } = widgetParams;
+ const { escapeHTML = true, indices = [] } = widgetParams;
// user passed a wrong `indices` option type
if (!Array.isArray(indices)) {
@@ -63,7 +60,7 @@ export default function connectAutocomplete(renderFn, unmountFn) {
return {
getConfiguration() {
- return widgetParams.escapeHits ? tagConfig : undefined;
+ return escapeHTML ? TAG_PLACEHOLDER : undefined;
},
init({ instantSearchInstance, helper }) {
@@ -106,11 +103,7 @@ export default function connectAutocomplete(renderFn, unmountFn) {
saveResults({ results, label }) {
const derivedIndex = this.indices.find(i => i.label === label);
- if (
- widgetParams.escapeHits &&
- results.hits &&
- results.hits.length > 0
- ) {
+ if (escapeHTML && results && results.hits && results.hits.length > 0) {
results.hits = escapeHits(results.hits);
}
diff --git a/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js b/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
index e2e0fed69c..91816fb088 100644
--- a/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
+++ b/src/connectors/breadcrumb/__tests__/connectBreadcrumb-test.js
@@ -4,7 +4,7 @@ const SearchResults = jsHelper.SearchResults;
import connectBreadcrumb from '../connectBreadcrumb.js';
describe('connectBreadcrumb', () => {
- it('It should compute getConfiguration() correctly', () => {
+ it('should compute getConfiguration() correctly', () => {
const rendering = jest.fn();
const makeWidget = connectBreadcrumb(rendering);
@@ -55,6 +55,28 @@ describe('connectBreadcrumb', () => {
}
});
+ it('should compute getConfiguration() correctly with a custom separator', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectBreadcrumb(rendering);
+
+ const widget = makeWidget({
+ attributes: ['category', 'sub_category'],
+ separator: ' / ',
+ });
+ const widgetConfiguration = widget.getConfiguration({});
+
+ expect(widgetConfiguration).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' / ',
+ },
+ ],
+ });
+ });
+
it('Renders during init and render', () => {
const rendering = jest.fn();
const makeWidget = connectBreadcrumb(rendering);
@@ -198,7 +220,7 @@ describe('connectBreadcrumb', () => {
const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toEqual([
- { name: 'Decoration', value: null },
+ { label: 'Decoration', value: null },
]);
});
@@ -251,7 +273,7 @@ describe('connectBreadcrumb', () => {
const widget = makeWidget({
attributes: ['category', 'sub_category'],
transformItems: items =>
- items.map(item => ({ ...item, name: 'transformed' })),
+ items.map(item => ({ ...item, label: 'transformed' })),
});
const config = widget.getConfiguration({});
@@ -299,7 +321,7 @@ describe('connectBreadcrumb', () => {
const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.items).toEqual([
- expect.objectContaining({ name: 'transformed' }),
+ expect.objectContaining({ label: 'transformed' }),
]);
});
diff --git a/src/connectors/breadcrumb/connectBreadcrumb.js b/src/connectors/breadcrumb/connectBreadcrumb.js
index f077fb702f..a3f44f2e03 100644
--- a/src/connectors/breadcrumb/connectBreadcrumb.js
+++ b/src/connectors/breadcrumb/connectBreadcrumb.js
@@ -24,7 +24,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} BreadcrumbItem
- * @property {string} name Name of the category or subcategory.
+ * @property {string} label Label of the category or subcategory.
* @property {string} value Value of breadcrumb item.
*/
@@ -83,7 +83,7 @@ export default function connectBreadcrumb(renderFn, unmountFn) {
isFacetSet.separator !== separator
) {
warn(
- 'Using Breadcrumb & HierarchicalMenu on the same facet with different options. Adding that one will override the configuration of the HierarchicalMenu. Check your options.'
+ 'Using Breadcrumb and HierarchicalMenu on the same facet with different options overrides the configuration of the HierarchicalMenu.'
);
}
return {};
@@ -181,7 +181,7 @@ function prepareItems(data) {
return data.reduce((result, currentItem) => {
if (currentItem.isRefined) {
result.push({
- name: currentItem.name,
+ label: currentItem.name,
value: currentItem.path,
});
if (Array.isArray(currentItem.data)) {
@@ -194,7 +194,7 @@ function prepareItems(data) {
function shiftItemsValues(array) {
return array.map((x, idx) => ({
- name: x.name,
+ label: x.label,
value: idx + 1 === array.length ? null : array[idx + 1].value,
}));
}
diff --git a/src/connectors/clear-all/__tests__/connectClearAll-test.js b/src/connectors/clear-all/__tests__/connectClearAll-test.js
deleted file mode 100644
index 9819601e9b..0000000000
--- a/src/connectors/clear-all/__tests__/connectClearAll-test.js
+++ /dev/null
@@ -1,443 +0,0 @@
-import jsHelper from 'algoliasearch-helper';
-const SearchResults = jsHelper.SearchResults;
-
-import connectClearAll from '../connectClearAll.js';
-
-describe('connectClearAll', () => {
- it('Renders during init and render', () => {
- const helper = jsHelper({});
- helper.search = () => {};
- // test that the dummyRendering is called with the isFirstRendering
- // flag set accordingly
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- foo: 'bar', // dummy param to test `widgetParams`
- });
-
- expect(widget.getConfiguration).toBe(undefined);
- // test if widget is not rendered yet at this point
- expect(rendering).toHaveBeenCalledTimes(0);
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- // test that rendering has been called during init with isFirstRendering = true
- expect(rendering).toHaveBeenCalledTimes(1);
- // test if isFirstRendering is true during init
- expect(rendering.mock.calls[0][1]).toBe(true);
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- expect(firstRenderingOptions.hasRefinements).toBe(false);
- expect(firstRenderingOptions.widgetParams).toEqual({
- foo: 'bar', // dummy param to test `widgetParams`
- });
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- // test that rendering has been called during init with isFirstRendering = false
- expect(rendering).toHaveBeenCalledTimes(2);
- expect(rendering.mock.calls[1][1]).toBe(false);
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- expect(secondRenderingOptions.hasRefinements).toBe(false);
- });
-
- it('Receives a mean to clear the values', () => {
- // test the function received by the rendering function
- // to clear the refinements
-
- const helper = jsHelper({}, '', {
- facets: ['myFacet'],
- });
- helper.search = () => {};
- helper.setQuery('not empty');
- helper.toggleRefinement('myFacet', 'myValue');
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({ clearsQuery: false });
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('myFacet')).toBe(true);
- expect(helper.state.query).toBe('not empty');
- const initClearMethod = rendering.mock.calls[0][0].refine;
- initClearMethod();
-
- expect(helper.hasRefinements('myFacet')).toBe(false);
- expect(helper.state.query).toBe('not empty');
-
- helper.toggleRefinement('myFacet', 'someOtherValue');
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(helper.hasRefinements('myFacet')).toBe(true);
- expect(helper.state.query).toBe('not empty');
- const renderClearMethod = rendering.mock.calls[1][0].refine;
- renderClearMethod();
- expect(helper.hasRefinements('myFacet')).toBe(false);
- expect(helper.state.query).toBe('not empty');
- });
-
- it('Receives a mean to clear the values (and the query)', () => {
- // test the function received by the rendering function
- // to clear the refinements
-
- const helper = jsHelper({}, '', {
- facets: ['myFacet'],
- });
- helper.search = () => {};
- helper.setQuery('a query');
- helper.toggleRefinement('myFacet', 'myValue');
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({ clearsQuery: true });
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('myFacet')).toBe(true);
- expect(helper.state.query).toBe('a query');
- const initClearMethod = rendering.mock.calls[0][0].refine;
- initClearMethod();
-
- expect(helper.hasRefinements('myFacet')).toBe(false);
- expect(helper.state.query).toBe('');
-
- helper.toggleRefinement('myFacet', 'someOtherValue');
- helper.setQuery('another query');
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(helper.hasRefinements('myFacet')).toBe(true);
- expect(helper.state.query).toBe('another query');
- const renderClearMethod = rendering.mock.calls[1][0].refine;
- renderClearMethod();
- expect(helper.hasRefinements('myFacet')).toBe(false);
- expect(helper.state.query).toBe('');
- });
-
- it('some refinements from results <-> hasRefinements = true', () => {
- // test if the values sent to the rendering function
- // are consistent with the search state
- const helper = jsHelper({}, undefined, {
- facets: ['aFacet'],
- });
- helper.toggleRefinement('aFacet', 'some value');
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(rendering.mock.calls[0][0].hasRefinements).toBe(true);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(rendering.mock.calls[1][0].hasRefinements).toBe(true);
- });
-
- it('(clearsQuery: true) query not empty <-> hasRefinements = true', () => {
- // test if the values sent to the rendering function
- // are consistent with the search state
- const helper = jsHelper({}, undefined, {
- facets: ['aFacet'],
- });
- helper.setQuery('no empty');
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- clearsQuery: true,
- });
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(rendering.mock.calls[0][0].hasRefinements).toBe(true);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(rendering.mock.calls[1][0].hasRefinements).toBe(true);
- });
-
- it('(clearsQuery: true) no refinements <-> hasRefinements = false', () => {
- // test if the values sent to the rendering function
- // are consistent with the search state
- const helper = jsHelper({}, undefined, {
- facets: ['aFacet'],
- });
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- clearsQuery: true,
- });
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(rendering.mock.calls[0][0].hasRefinements).toBe(false);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
- });
-
- it('(clearsQuery: false) no refinements <=> hasRefinements = false', () => {
- // test if the values sent to the rendering function
- // are consistent with the search state
-
- const helper = jsHelper({});
- helper.setQuery('not empty');
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({ clearsQuery: false });
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(rendering.mock.calls[0][0].hasRefinements).toBe(false);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
- });
-
- it('can exclude some attributes', () => {
- const helper = jsHelper({ addAlgoliaAgent: () => {} }, '', {
- facets: ['facet'],
- });
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- excludeAttributes: ['facet'],
- });
-
- helper.toggleRefinement('facet', 'value');
-
- {
- helper.setQuery('not empty');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('facet')).toBe(true);
-
- const refine = rendering.mock.calls[0][0].refine;
- refine();
-
- expect(helper.hasRefinements('facet')).toBe(true);
- }
-
- {
- // facet has not been cleared and it is still refined with value
- helper.setQuery('not empty');
-
- widget.render({
- helper,
- state: helper.state,
- results: new SearchResults(helper.state, [{}]),
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('facet')).toBe(true);
-
- const refine = rendering.mock.calls[1][0].refine;
- refine();
-
- expect(helper.hasRefinements('facet')).toBe(true);
- }
- });
-
- it('can exclude some attributes when clearsQuery is active', () => {
- const helper = jsHelper({ addAlgoliaAgent: () => {} }, '', {
- facets: ['facet'],
- });
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- excludeAttributes: ['facet'],
- clearsQuery: true,
- });
-
- helper.toggleRefinement('facet', 'value');
-
- {
- helper.setQuery('not empty');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('facet')).toBe(true);
-
- const refine = rendering.mock.calls[0][0].refine;
- refine();
-
- expect(helper.hasRefinements('facet')).toBe(true);
- }
-
- {
- helper.setQuery('not empty');
-
- widget.render({
- helper,
- state: helper.state,
- results: new SearchResults(helper.state, [{}]),
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- expect(helper.hasRefinements('facet')).toBe(true);
-
- const refine = rendering.mock.calls[1][0].refine;
- refine();
-
- expect(helper.hasRefinements('facet')).toBe(true);
- }
- });
-
- describe('createURL', () => {
- it('consistent with the list of excludedAttributes', () => {
- const helper = jsHelper({ addAlgoliaAgent: () => {} }, '', {
- facets: ['facet', 'otherFacet'],
- });
- helper.search = () => {};
-
- const rendering = jest.fn();
- const makeWidget = connectClearAll(rendering);
- const widget = makeWidget({
- excludeAttributes: ['facet'],
- clearsQuery: true,
- });
-
- helper.toggleRefinement('facet', 'value');
- helper.toggleRefinement('otherFacet', 'value');
-
- {
- helper.setQuery('not empty');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: opts => opts,
- onHistoryChange: () => {},
- });
-
- const { createURL, refine } = rendering.mock.calls[0][0];
-
- // The state represented by the URL should be equal to a state
- // after refining.
- const createURLState = createURL();
- refine();
- const stateAfterRefine = helper.state;
-
- expect(createURLState).toEqual(stateAfterRefine);
- }
-
- {
- widget.render({
- helper,
- state: helper.state,
- results: new SearchResults(helper.state, [{}]),
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const { createURL, refine } = rendering.mock.calls[1][0];
-
- const createURLState = createURL();
- refine();
- const stateAfterRefine = helper.state;
-
- expect(createURLState).toEqual(stateAfterRefine);
- }
- });
- });
-});
diff --git a/src/connectors/clear-all/connectClearAll.js b/src/connectors/clear-all/connectClearAll.js
deleted file mode 100644
index 994a6265b9..0000000000
--- a/src/connectors/clear-all/connectClearAll.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import {
- checkRendering,
- clearRefinements,
- getAttributesToClear,
-} from '../../lib/utils.js';
-
-const usage = `Usage:
-var customClearAll = connectClearAll(function render(params, isFirstRendering) {
- // params = {
- // refine,
- // hasRefinements,
- // createURL,
- // instantSearchInstance,
- // widgetParams,
- // }
-});
-search.addWidget(
- customClearAll({
- [ excludeAttributes = [] ],
- [ clearsQuery = false ]
- })
-);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectClearAll.html
-`;
-
-/**
- * @typedef {Object} CustomClearAllWidgetOptions
- * @property {string[]} [excludeAttributes = []] Every attributes that should not be removed when calling `refine()`.
- * @property {boolean} [clearsQuery = false] If `true`, `refine()` also clears the current search query.
- */
-
-/**
- * @typedef {Object} ClearAllRenderingOptions
- * @property {function} refine Triggers the clear of all the currently refined values.
- * @property {boolean} hasRefinements Indicates if search state is refined.
- * @property {function} createURL Creates a url for the next state when refinements are cleared.
- * @property {Object} widgetParams All original `CustomClearAllWidgetOptions` forwarded to the `renderFn`.
- */
-
-/**
- * **ClearAll** connector provides the logic to build a custom widget that will give the user
- * the ability to reset the search state.
- *
- * This connector provides a `refine` function to remove the current refined facets.
- *
- * The behaviour of this function can be changed with widget options. If `clearsQuery`
- * is set to `true`, `refine` will also clear the query and `excludeAttributes` can
- * prevent certain attributes from being cleared.
- *
- * @type {Connector}
- * @param {function(ClearAllRenderingOptions, boolean)} renderFn Rendering function for the custom **ClearAll** widget.
- * @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomClearAllWidgetOptions)} Re-usable widget factory for a custom **ClearAll** widget.
- * @example
- * // custom `renderFn` to render the custom ClearAll widget
- * function renderFn(ClearAllRenderingOptions, isFirstRendering) {
- * var containerNode = ClearAllRenderingOptions.widgetParams.containerNode;
- * if (isFirstRendering) {
- * var markup = $('
Clear All ');
- * containerNode.append(markup);
- *
- * markup.on('click', function(event) {
- * event.preventDefault();
- * ClearAllRenderingOptions.refine();
- * })
- * }
- *
- * var clearAllCTA = containerNode.find('#custom-clear-all');
- * clearAllCTA.attr('disabled', !ClearAllRenderingOptions.hasRefinements)
- * };
- *
- * // connect `renderFn` to ClearAll logic
- * var customClearAllWidget = instantsearch.connectors.connectClearAll(renderFn);
- *
- * // mount widget on the page
- * search.addWidget(
- * customClearAllWidget({
- * containerNode: $('#custom-clear-all-container'),
- * })
- * );
- */
-export default function connectClearAll(renderFn, unmountFn) {
- checkRendering(renderFn, usage);
-
- return (widgetParams = {}) => {
- const { excludeAttributes = [], clearsQuery = false } = widgetParams;
-
- return {
- init({ helper, instantSearchInstance, createURL }) {
- const attributesToClear = getAttributesToClear({
- helper,
- blackList: excludeAttributes,
- });
-
- const hasRefinements = clearsQuery
- ? attributesToClear.length !== 0 || helper.state.query !== ''
- : attributesToClear.length !== 0;
-
- this._refine = () => {
- helper
- .setState(
- clearRefinements({
- helper,
- blackList: excludeAttributes,
- clearsQuery,
- })
- )
- .search();
- };
-
- this._createURL = () =>
- createURL(
- clearRefinements({
- helper,
- blackList: excludeAttributes,
- clearsQuery,
- })
- );
-
- renderFn(
- {
- refine: this._refine,
- hasRefinements,
- createURL: this._createURL,
- instantSearchInstance,
- widgetParams,
- },
- true
- );
- },
-
- render({ helper, instantSearchInstance }) {
- const attributesToClear = getAttributesToClear({
- helper,
- blackList: excludeAttributes,
- });
-
- const hasRefinements = clearsQuery
- ? attributesToClear.length !== 0 || helper.state.query !== ''
- : attributesToClear.length !== 0;
-
- renderFn(
- {
- refine: this._refine,
- hasRefinements,
- createURL: this._createURL,
- instantSearchInstance,
- widgetParams,
- },
- false
- );
- },
-
- dispose() {
- unmountFn();
- },
- };
- };
-}
diff --git a/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js
new file mode 100644
index 0000000000..e4703f6098
--- /dev/null
+++ b/src/connectors/clear-refinements/__tests__/connectClearRefinements-test.js
@@ -0,0 +1,511 @@
+import jsHelper from 'algoliasearch-helper';
+const SearchResults = jsHelper.SearchResults;
+
+import connectClearRefinements from '../connectClearRefinements';
+
+describe('connectClearRefinements', () => {
+ describe('Usage', () => {
+ it('throws if given both `includedAttributes` and `excludedAttributes`', () => {
+ const customClearRefinements = connectClearRefinements(() => {});
+
+ expect(
+ customClearRefinements.bind(null, {
+ includedAttributes: ['query'],
+ excludedAttributes: ['brand'],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\`includedAttributes\` and \`excludedAttributes\` cannot be used together."`
+ );
+ });
+ });
+
+ describe('Lifecycle', () => {
+ it('renders during init and render', () => {
+ const helper = jsHelper({});
+ helper.search = () => {};
+ // test that the dummyRendering is called with the isFirstRendering
+ // flag set accordingly
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ foo: 'bar', // dummy param to test `widgetParams`
+ });
+
+ expect(widget.getConfiguration).toBe(undefined);
+ // test if widget is not rendered yet at this point
+ expect(rendering).toHaveBeenCalledTimes(0);
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ // test that rendering has been called during init with isFirstRendering = true
+ expect(rendering).toHaveBeenCalledTimes(1);
+ // test if isFirstRendering is true during init
+ expect(rendering.mock.calls[0][1]).toBe(true);
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.hasRefinements).toBe(false);
+ expect(firstRenderingOptions.widgetParams).toEqual({
+ foo: 'bar', // dummy param to test `widgetParams`
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ // test that rendering has been called during init with isFirstRendering = false
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering.mock.calls[1][1]).toBe(false);
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.hasRefinements).toBe(false);
+ });
+ });
+
+ describe('Instance options', () => {
+ it('provides a function to clear the refinements', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['myFacet'],
+ });
+ helper.search = () => {};
+ helper.setQuery('not empty');
+ helper.toggleRefinement('myFacet', 'myValue');
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({});
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('myFacet')).toBe(true);
+ expect(helper.state.query).toBe('not empty');
+ const initClearMethod = rendering.mock.calls[0][0].refine;
+ initClearMethod();
+
+ expect(helper.hasRefinements('myFacet')).toBe(false);
+ expect(helper.state.query).toBe('not empty');
+
+ helper.toggleRefinement('myFacet', 'someOtherValue');
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('myFacet')).toBe(true);
+ expect(helper.state.query).toBe('not empty');
+ const renderClearMethod = rendering.mock.calls[1][0].refine;
+ renderClearMethod();
+ expect(helper.hasRefinements('myFacet')).toBe(false);
+ expect(helper.state.query).toBe('not empty');
+ });
+
+ it('provides a function to clear the refinements and the query', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['myFacet'],
+ });
+ helper.search = () => {};
+ helper.setQuery('a query');
+ helper.toggleRefinement('myFacet', 'myValue');
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({ excludedAttributes: [] });
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('myFacet')).toBe(true);
+ expect(helper.state.query).toBe('a query');
+ const initClearMethod = rendering.mock.calls[0][0].refine;
+ initClearMethod();
+
+ expect(helper.hasRefinements('myFacet')).toBe(false);
+ expect(helper.state.query).toBe('');
+
+ helper.toggleRefinement('myFacet', 'someOtherValue');
+ helper.setQuery('another query');
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('myFacet')).toBe(true);
+ expect(helper.state.query).toBe('another query');
+ const renderClearMethod = rendering.mock.calls[1][0].refine;
+ renderClearMethod();
+ expect(helper.hasRefinements('myFacet')).toBe(false);
+ expect(helper.state.query).toBe('');
+ });
+
+ it('gets refinements from results', () => {
+ const helper = jsHelper({}, undefined, {
+ facets: ['aFacet'],
+ });
+ helper.toggleRefinement('aFacet', 'some value');
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].hasRefinements).toBe(true);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(true);
+ });
+
+ it('with query not excluded and not empty has refinements', () => {
+ // test if the values sent to the rendering function
+ // are consistent with the search state
+ const helper = jsHelper({}, undefined, {
+ facets: ['aFacet'],
+ });
+ helper.setQuery('no empty');
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ excludedAttributes: [],
+ });
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].hasRefinements).toBe(true);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(true);
+ });
+
+ it('with query not excluded and empty has no refinements', () => {
+ const helper = jsHelper({}, undefined, {
+ facets: ['aFacet'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ excludedAttributes: [],
+ });
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].hasRefinements).toBe(false);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
+ });
+
+ it('without includedAttributes or excludedAttributes and with a query has no refinements', () => {
+ const helper = jsHelper({});
+ helper.setQuery('not empty');
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({});
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].hasRefinements).toBe(false);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
+ });
+
+ it('includes only includedAttributes', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['facet1', 'facet2'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({ includedAttributes: ['facet1'] });
+
+ helper
+ .toggleRefinement('facet1', 'value')
+ .toggleRefinement('facet2', 'value')
+ .setQuery('not empty');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(true);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+
+ const refine = rendering.mock.calls[0][0].refine;
+ refine();
+ widget.render({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
+ });
+
+ it('includes only includedAttributes (with query)', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['facet1'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({ includedAttributes: ['facet1', 'query'] });
+
+ helper.toggleRefinement('facet1', 'value').setQuery('not empty');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(true);
+ expect(helper.getState().query).toBe('not empty');
+
+ const refine = rendering.mock.calls[0][0].refine;
+ refine();
+ widget.render({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ expect(helper.getState().query).toBe('');
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
+ });
+
+ it('excludes excludedAttributes', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['facet1', 'facet2'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ excludedAttributes: ['facet2'],
+ });
+
+ helper
+ .toggleRefinement('facet1', 'value')
+ .toggleRefinement('facet2', 'value');
+
+ {
+ helper.setQuery('not empty');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(true);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+
+ const refine = rendering.mock.calls[0][0].refine;
+ refine();
+
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+
+ expect(rendering.mock.calls[0][0].hasRefinements).toBe(true);
+ }
+
+ {
+ // facet has not been cleared and it is still refined with value
+ helper.setQuery('not empty');
+
+ widget.render({
+ helper,
+ state: helper.state,
+ results: new SearchResults(helper.state, [{}]),
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+
+ const refine = rendering.mock.calls[1][0].refine;
+ refine();
+
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+ }
+ });
+
+ describe('transformItems is called', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['facet1', 'facet2', 'facet3'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ includedAttributes: ['facet2', 'facet3', 'query'],
+ transformItems: items =>
+ items.filter(
+ attribute => attribute === 'query' || attribute === 'facet3'
+ ),
+ });
+
+ helper
+ .toggleRefinement('facet1', 'value')
+ .toggleRefinement('facet2', 'value')
+ .toggleRefinement('facet3', 'value')
+ .setQuery('not empty');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(true);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+ expect(helper.hasRefinements('facet3')).toBe(true);
+ expect(helper.getState().query).toBe('not empty');
+
+ const refine = rendering.mock.calls[0][0].refine;
+ refine();
+ widget.render({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(helper.hasRefinements('facet1')).toBe(true);
+ expect(helper.hasRefinements('facet2')).toBe(true);
+ expect(helper.hasRefinements('facet3')).toBe(false);
+ expect(helper.getState().query).toBe('');
+ expect(rendering.mock.calls[1][0].hasRefinements).toBe(false);
+ });
+
+ describe('createURL', () => {
+ it('consistent with the list of excludedAttributes', () => {
+ const helper = jsHelper({}, '', {
+ facets: ['facet', 'otherFacet'],
+ });
+ helper.search = () => {};
+
+ const rendering = jest.fn();
+ const makeWidget = connectClearRefinements(rendering);
+ const widget = makeWidget({
+ excludedAttributes: ['facet'],
+ });
+
+ helper.toggleRefinement('facet', 'value');
+ helper.toggleRefinement('otherFacet', 'value');
+
+ {
+ helper.setQuery('not empty');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: opts => opts,
+ });
+
+ const { createURL, refine } = rendering.mock.calls[0][0];
+
+ // The state represented by the URL should be equal to a state
+ // after refining.
+ const createURLState = createURL();
+ refine();
+ const stateAfterRefine = helper.state;
+
+ expect(createURLState).toEqual(stateAfterRefine);
+ }
+
+ {
+ widget.render({
+ helper,
+ state: helper.state,
+ results: new SearchResults(helper.state, [{}]),
+ createURL: () => '#',
+ });
+
+ const { createURL, refine } = rendering.mock.calls[1][0];
+
+ const createURLState = createURL();
+ refine();
+ const stateAfterRefine = helper.state;
+
+ expect(createURLState).toEqual(stateAfterRefine);
+ }
+ });
+ });
+ });
+});
diff --git a/src/connectors/clear-refinements/connectClearRefinements.js b/src/connectors/clear-refinements/connectClearRefinements.js
new file mode 100644
index 0000000000..0be8e51030
--- /dev/null
+++ b/src/connectors/clear-refinements/connectClearRefinements.js
@@ -0,0 +1,207 @@
+import {
+ checkRendering,
+ clearRefinements,
+ getRefinements,
+} from '../../lib/utils.js';
+
+const usage = `Usage:
+var customClearRefinements = connectClearRefinements(function render(params, isFirstRendering) {
+ // params = {
+ // refine,
+ // hasRefinements,
+ // createURL,
+ // instantSearchInstance,
+ // widgetParams,
+ // }
+});
+search.addWidget(
+ customClearRefinements({
+ [ includedAttributes = [] ],
+ [ excludedAttributes = ['query'] ],
+ [ transformItems ],
+ })
+);
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectClearRefinements.html
+`;
+
+/**
+ * @typedef {Object} CustomClearRefinementsWidgetOptions
+ * @property {string[]} [includedAttributes = []] The attributes to include in the refinements to clear (all by default). Cannot be used with `excludedAttributes`.
+ * @property {string[]} [excludedAttributes = ['query']] The attributes to exclude from the refinements to clear. Cannot be used with `includedAttributes`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * @typedef {Object} ClearRefinementsRenderingOptions
+ * @property {function} refine Triggers the clear of all the currently refined values.
+ * @property {boolean} hasRefinements Indicates if search state is refined.
+ * @property {function} createURL Creates a url for the next state when refinements are cleared.
+ * @property {Object} widgetParams All original `CustomClearRefinementsWidgetOptions` forwarded to the `renderFn`.
+ */
+
+/**
+ * **ClearRefinements** connector provides the logic to build a custom widget that will give the user
+ * the ability to reset the search state.
+ *
+ * This connector provides a `refine` function to remove the current refined facets.
+ *
+ * The behaviour of this function can be changed with widget options. If `clearsQuery`
+ * is set to `true`, `refine` will also clear the query and `excludedAttributes` can
+ * prevent certain attributes from being cleared.
+ *
+ * @type {Connector}
+ * @param {function(ClearRefinementsRenderingOptions, boolean)} renderFn Rendering function for the custom **ClearRefinements** widget.
+ * @param {function} unmountFn Unmount function called when the widget is disposed.
+ * @return {function(CustomClearRefinementsWidgetOptions)} Re-usable widget factory for a custom **ClearRefinements** widget.
+ * @example
+ * // custom `renderFn` to render the custom ClearRefinements widget
+ * function renderFn(ClearRefinementsRenderingOptions, isFirstRendering) {
+ * var containerNode = ClearRefinementsRenderingOptions.widgetParams.containerNode;
+ * if (isFirstRendering) {
+ * var markup = $('
Clear All ');
+ * containerNode.append(markup);
+ *
+ * markup.on('click', function(event) {
+ * event.preventDefault();
+ * ClearRefinementsRenderingOptions.refine();
+ * })
+ * }
+ *
+ * var clearRefinementsCTA = containerNode.find('#custom-clear-all');
+ * clearRefinementsCTA.attr('disabled', !ClearRefinementsRenderingOptions.hasRefinements)
+ * };
+ *
+ * // connect `renderFn` to ClearRefinements logic
+ * var customClearRefinementsWidget = instantsearch.connectors.connectClearRefinements(renderFn);
+ *
+ * // mount widget on the page
+ * search.addWidget(
+ * customClearRefinementsWidget({
+ * containerNode: $('#custom-clear-all-container'),
+ * })
+ * );
+ */
+export default function connectClearRefinements(renderFn, unmountFn) {
+ checkRendering(renderFn, usage);
+
+ return (widgetParams = {}) => {
+ if (widgetParams.includedAttributes && widgetParams.excludedAttributes) {
+ throw new Error(
+ '`includedAttributes` and `excludedAttributes` cannot be used together.'
+ );
+ }
+
+ const {
+ includedAttributes = [],
+ excludedAttributes = ['query'],
+ transformItems = items => items,
+ } = widgetParams;
+
+ return {
+ init({ helper, instantSearchInstance, createURL }) {
+ const attributesToClear = getAttributesToClear({
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ });
+ const hasRefinements = attributesToClear.length > 0;
+
+ this._refine = () => {
+ helper
+ .setState(
+ clearRefinements({
+ helper,
+ attributesToClear: getAttributesToClear({
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ }),
+ })
+ )
+ .search();
+ };
+
+ this._createURL = () =>
+ createURL(
+ clearRefinements({
+ helper,
+ attributesToClear: getAttributesToClear({
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ }),
+ })
+ );
+
+ renderFn(
+ {
+ hasRefinements,
+ refine: this._refine,
+ createURL: this._createURL,
+ instantSearchInstance,
+ widgetParams,
+ },
+ true
+ );
+ },
+
+ render({ helper, instantSearchInstance }) {
+ const attributesToClear = getAttributesToClear({
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ });
+ const hasRefinements = attributesToClear.length > 0;
+
+ renderFn(
+ {
+ hasRefinements,
+ refine: this._refine,
+ createURL: this._createURL,
+ instantSearchInstance,
+ widgetParams,
+ },
+ false
+ );
+ },
+
+ dispose() {
+ unmountFn();
+ },
+ };
+ };
+}
+
+function getAttributesToClear({
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+}) {
+ const clearsQuery =
+ includedAttributes.indexOf('query') !== -1 ||
+ excludedAttributes.indexOf('query') === -1;
+
+ return transformItems(
+ getRefinements(helper.lastResults || {}, helper.state, clearsQuery)
+ .map(refinement => refinement.attributeName)
+ .filter(
+ attribute =>
+ // If the array is empty (default case), we keep all the attributes
+ includedAttributes.length === 0 ||
+ // Otherwise, only add the specified attributes
+ includedAttributes.indexOf(attribute) !== -1
+ )
+ .filter(
+ attribute =>
+ // If the query is included, we ignore the default `excludedAttributes = ['query']`
+ (attribute === 'query' && clearsQuery) ||
+ // Otherwise, ignore the excluded attributes
+ excludedAttributes.indexOf(attribute) === -1
+ )
+ );
+}
diff --git a/src/connectors/configure/__tests__/connectConfigure-test.js b/src/connectors/configure/__tests__/connectConfigure-test.js
index f031e04ff8..410e5b066a 100644
--- a/src/connectors/configure/__tests__/connectConfigure-test.js
+++ b/src/connectors/configure/__tests__/connectConfigure-test.js
@@ -13,18 +13,22 @@ describe('connectConfigure', () => {
helper = algoliasearchHelper(fakeClient, '', {});
});
- describe('throws on bad usage', () => {
- it('without searchParameters', () => {
+ describe('Usage', () => {
+ it('throws without searchParameters', () => {
const makeWidget = connectConfigure();
expect(() => makeWidget()).toThrow();
});
- it('with a renderFn but no unmountFn', () => {
- expect(() => connectConfigure(jest.fn(), undefined)).toThrow();
+ it('does not throw with a render function but without an unmount function', () => {
+ expect(() => connectConfigure(jest.fn(), undefined)).not.toThrow();
});
- it('with a unmountFn but no renderFn', () => {
- expect(() => connectConfigure(undefined, jest.fn())).toThrow();
+ it('with a unmount function but no render function does not throw', () => {
+ expect(() => connectConfigure(undefined, jest.fn())).not.toThrow();
+ });
+
+ it('does not throw without render and unmount functions', () => {
+ expect(() => connectConfigure(undefined, undefined)).not.toThrow();
});
});
diff --git a/src/connectors/configure/connectConfigure.js b/src/connectors/configure/connectConfigure.js
index cb600510b6..cb72895c0a 100644
--- a/src/connectors/configure/connectConfigure.js
+++ b/src/connectors/configure/connectConfigure.js
@@ -1,18 +1,25 @@
-import isFunction from 'lodash/isFunction';
+import noop from 'lodash/noop';
import isPlainObject from 'lodash/isPlainObject';
import { enhanceConfiguration } from '../../lib/InstantSearch.js';
const usage = `Usage:
-var customConfigureWidget = connectConfigure(
+var customConfigure = connectConfigure(
function renderFn(params, isFirstRendering) {
// params = {
// refine,
- // widgetParams
+ // widgetParams,
// }
- },
- function disposeFn() {}
-)
+ }
+);
+search.addWidget(
+ customConfigure({
+ searchParameters: {
+ // any search parameter: https://www.algolia.com/doc/api-reference/search-api-parameters/
+ }
+ })
+);
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectConfigure.html
`;
/**
@@ -35,14 +42,7 @@ var customConfigureWidget = connectConfigure(
* @param {function} unmountFn Unmount function called when the widget is disposed.
* @return {function(CustomConfigureWidgetOptions)} Re-usable widget factory for a custom **Configure** widget.
*/
-export default function connectConfigure(renderFn, unmountFn) {
- if (
- (isFunction(renderFn) && !isFunction(unmountFn)) ||
- (!isFunction(renderFn) && isFunction(unmountFn))
- ) {
- throw new Error(usage);
- }
-
+export default function connectConfigure(renderFn = noop, unmountFn = noop) {
return (widgetParams = {}) => {
if (!isPlainObject(widgetParams.searchParameters)) {
throw new Error(usage);
@@ -56,15 +56,13 @@ export default function connectConfigure(renderFn, unmountFn) {
init({ helper }) {
this._refine = this.refine(helper);
- if (isFunction(renderFn)) {
- renderFn(
- {
- refine: this._refine,
- widgetParams,
- },
- true
- );
- }
+ renderFn(
+ {
+ refine: this._refine,
+ widgetParams,
+ },
+ true
+ );
},
refine(helper) {
@@ -87,19 +85,18 @@ export default function connectConfigure(renderFn, unmountFn) {
},
render() {
- if (renderFn) {
- renderFn(
- {
- refine: this._refine,
- widgetParams,
- },
- false
- );
- }
+ renderFn(
+ {
+ refine: this._refine,
+ widgetParams,
+ },
+ false
+ );
},
dispose({ state }) {
- if (unmountFn) unmountFn();
+ unmountFn();
+
return this.removeSearchParameters(state);
},
diff --git a/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js b/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js
deleted file mode 100644
index 077439509f..0000000000
--- a/src/connectors/current-refined-values/__tests__/connectCurrentRefinedValues-test.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import jsHelper from 'algoliasearch-helper';
-const SearchResults = jsHelper.SearchResults;
-import connectCurrentRefinedValues from '../connectCurrentRefinedValues.js';
-
-describe('connectCurrentRefinedValues', () => {
- it('Renders during init and render', () => {
- const helper = jsHelper({});
- helper.search = () => {};
- // test that the dummyRendering is called with the isFirstRendering
- // flag set accordingly
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({
- foo: 'bar', // dummy param to test `widgetParams`
- });
-
- expect(widget.getConfiguration).toBe(undefined);
- // test if widget is not rendered yet at this point
- expect(rendering).toHaveBeenCalledTimes(0);
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- // test that rendering has been called during init with isFirstRendering = true
- expect(rendering).toHaveBeenCalledTimes(1);
- // test if isFirstRendering is true during init
- expect(rendering.mock.calls[0][1]).toBe(true);
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- expect(firstRenderingOptions.refinements).toEqual([]);
- expect(firstRenderingOptions.widgetParams).toEqual({
- foo: 'bar',
- });
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- // test that rendering has been called during init with isFirstRendering = false
- expect(rendering).toHaveBeenCalledTimes(2);
- expect(rendering.mock.calls[1][1]).toBe(false);
-
- const secondRenderingOptions = rendering.mock.calls[0][0];
- expect(secondRenderingOptions.refinements).toEqual([]);
- expect(secondRenderingOptions.widgetParams).toEqual({
- foo: 'bar',
- });
- });
-
- it('Renders transformed items during init and render', () => {
- const helper = jsHelper({}, '', {
- facets: ['myFacet'],
- });
- helper.search = () => {};
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({
- transformItems: items =>
- items.map(item => ({ ...item, name: 'transformed' })),
- });
-
- helper.addFacetRefinement('myFacet', 'value');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- expect(firstRenderingOptions.refinements).toEqual([
- expect.objectContaining({ name: 'transformed' }),
- ]);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[0][0];
- expect(secondRenderingOptions.refinements).toEqual([
- expect.objectContaining({ name: 'transformed' }),
- ]);
- });
-
- it('Provide a function to clear the refinement', () => {
- // For each refinements we get a function that we can call
- // for removing a single refinement
- const helper = jsHelper({}, '', {
- facets: ['myFacet'],
- });
- helper.search = () => {};
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget();
-
- helper.addFacetRefinement('myFacet', 'value');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const refinements = firstRenderingOptions.refinements;
- expect(typeof firstRenderingOptions.refine).toBe('function');
- expect(refinements).toHaveLength(1);
- firstRenderingOptions.refine(refinements[0]);
- expect(helper.hasRefinements('myFacet')).toBe(false);
-
- helper.addFacetRefinement('myFacet', 'value');
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- const otherRefinements = secondRenderingOptions.refinements;
- expect(typeof secondRenderingOptions.refine).toBe('function');
- expect(otherRefinements).toHaveLength(1);
- secondRenderingOptions.refine(refinements[0]);
- expect(helper.hasRefinements('myFacet')).toBe(false);
- });
-
- it('should clear also the search query', () => {
- const helper = jsHelper({}, '', {
- facets: ['myFacet'],
- });
- helper.search = jest.fn();
-
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({ clearsQuery: true });
-
- helper.setQuery('foobar');
- helper.toggleRefinement('myFacet', 'value');
- expect(helper.state.query).toBe('foobar');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- // clear current refined values + query
- expect(rendering).toBeCalled();
- expect(helper.hasRefinements('myFacet')).toBe(true);
-
- const [{ clearAllClick }] = rendering.mock.calls[0];
- clearAllClick();
-
- expect(helper.search).toBeCalled();
- expect(helper.state.query).toBe('');
- expect(helper.hasRefinements('myFacet')).toBe(false);
- });
-
- it('should provide the query as a refinement if clearsQuery is true', () => {
- const helper = jsHelper({}, '', {});
- helper.search = jest.fn();
-
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({ clearsQuery: true });
-
- helper.setQuery('foobar');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const refinements = firstRenderingOptions.refinements;
- expect(refinements).toHaveLength(1);
- const value = refinements[0];
- expect(value.type).toBe('query');
- expect(value.name).toBe('foobar');
- expect(value.query).toBe('foobar');
- const refine = firstRenderingOptions.refine;
- refine(value);
- expect(helper.state.query).toBe('');
-
- helper.setQuery('foobaz');
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- const secondRefinements = secondRenderingOptions.refinements;
- expect(secondRefinements).toHaveLength(1);
- const secondValue = secondRefinements[0];
- expect(secondValue.type).toBe('query');
- expect(secondValue.name).toBe('foobaz');
- expect(secondValue.query).toBe('foobaz');
- const secondRefine = secondRenderingOptions.refine;
- secondRefine(secondValue);
- expect(helper.state.query).toBe('');
- });
-
- it('should provide the query as a refinement if clearsQuery is true, onlyListedAttributes is true and `query` is a listed attribute', () => {
- const helper = jsHelper({}, '', {});
-
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({
- clearsQuery: true,
- onlyListedAttributes: true,
- attributes: [{ name: 'query' }],
- });
-
- helper.setQuery('foobar');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const refinements = firstRenderingOptions.refinements;
- expect(refinements).toHaveLength(1);
- const value = refinements[0];
- expect(value.type).toBe('query');
- expect(value.name).toBe('foobar');
- expect(value.query).toBe('foobar');
- });
-
- it('should not provide the query as a refinement if clearsQuery is true, onlyListedAttributes is true but query is not listed in attributes', () => {
- const helper = jsHelper({}, '', {});
-
- const rendering = jest.fn();
- const makeWidget = connectCurrentRefinedValues(rendering);
- const widget = makeWidget({
- clearsQuery: true,
- onlyListedAttributes: true,
- attributes: [{ name: 'brand' }],
- });
-
- helper.setQuery('foobar');
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const refinements = firstRenderingOptions.refinements;
- expect(refinements).toHaveLength(0);
- });
-});
diff --git a/src/connectors/current-refined-values/connectCurrentRefinedValues.js b/src/connectors/current-refined-values/connectCurrentRefinedValues.js
deleted file mode 100644
index cfcb7787df..0000000000
--- a/src/connectors/current-refined-values/connectCurrentRefinedValues.js
+++ /dev/null
@@ -1,407 +0,0 @@
-import isUndefined from 'lodash/isUndefined';
-import isBoolean from 'lodash/isBoolean';
-import isString from 'lodash/isString';
-import isArray from 'lodash/isArray';
-import isPlainObject from 'lodash/isPlainObject';
-import isFunction from 'lodash/isFunction';
-import isEmpty from 'lodash/isEmpty';
-
-import map from 'lodash/map';
-import reduce from 'lodash/reduce';
-import filter from 'lodash/filter';
-
-import {
- getRefinements,
- clearRefinements,
- checkRendering,
-} from '../../lib/utils.js';
-
-const usage = `Usage:
-var customCurrentRefinedValues = connectCurrentRefinedValues(function renderFn(params, isFirstRendering) {
- // params = {
- // attributes,
- // clearAllClick,
- // clearAllPosition,
- // clearAllURL,
- // refine,
- // createURL,
- // refinements,
- // instantSearchInstance,
- // widgetParams,
- // }
-});
-search.addWidget(
- customCurrentRefinedValues({
- [ attributes = [] ],
- [ onlyListedAttributes = false ],
- [ clearsQuery = false ],
- [ transformItems ],
- })
-);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectCurrentRefinedValues.html
-`;
-
-/**
- * @typedef {Object} CurrentRefinement
- * @property {"facet"|"exclude"|"disjunctive"|"hierarchical"|"numeric"|"query"} type Type of refinement
- * @property {string} attributeName Attribute on which the refinement is applied
- * @property {string} name value of the refinement
- * @property {number} [numericValue] value if the attribute is numeric and used with a numeric filter
- * @property {boolean} [exhaustive] `true` if the count is exhaustive, only if applicable
- * @property {number} [count] number of items found, if applicable
- * @property {string} [query] value of the query if the type is query
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesRenderingOptions
- * @property {Object.
} attributes Original `CurrentRefinedValuesWidgetOptions.attributes` mapped by keys.
- * @property {function} clearAllClick Clears all the currently refined values.
- * @property {function} clearAllURL Generate a URL which leads to a state where all the refinements have been cleared.
- * @property {function(item)} refine Clears a single refinement.
- * @property {function(item): string} createURL Creates an individual url where a single refinement is cleared.
- * @property {CurrentRefinement[]} refinements All the current refinements.
- * @property {Object} widgetParams All original `CustomCurrentRefinedValuesWidgetOptions` forwarded to the `renderFn`.
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesAttributes
- * @property {string} name Mandatory field which is the name of the attribute.
- * @property {string} label The label to apply on a refinement per attribute.
- */
-
-/**
- * @typedef {Object} CustomCurrentRefinedValuesWidgetOptions
- * @property {CurrentRefinedValuesAttributes[]} [attributes = []] Specification for the display of
- * refinements per attribute (default: `[]`). By default, the widget will display all the filters
- * set with no special treatment for the label.
- * @property {boolean} [onlyListedAttributes = false] Limit the displayed refinement to the list specified.
- * @property {boolean} [clearsQuery = false] Clears also the active search query when using clearAll.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * **CurrentRefinedValues** connector provides the logic to build a widget that will give
- * the user the ability to see all the currently applied filters and, remove some or all of
- * them.
- *
- * This provides a `refine(item)` function to remove a selected refinement and a `clearAllClick`
- * function to clear all the filters. Those functions can see their behaviour change based on
- * the widget options used.
- * @type {Connector}
- * @param {function(CurrentRefinedValuesRenderingOptions)} renderFn Rendering function for the custom **CurrentRefinedValues** widget.
- * @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomCurrentRefinedValuesWidgetOptions)} Re-usable widget factory for a custom **CurrentRefinedValues** widget.
- * @example
- * // custom `renderFn` to render the custom ClearAll widget
- * function renderFn(CurrentRefinedValuesRenderingOptions, isFirstRendering) {
- * var containerNode = CurrentRefinedValuesRenderingOptions.widgetParams.containerNode;
- * if (isFirstRendering) {
- * containerNode
- * .html('
');
- * }
- *
- * containerNode
- * .find('#cta-container > a')
- * .off('click');
- *
- * containerNode
- * .find('li > a')
- * .each(function() { $(this).off('click') });
- *
- * if (CurrentRefinedValuesRenderingOptions.refinements
- * && CurrentRefinedValuesRenderingOptions.refinements.length > 0) {
- * containerNode
- * .find('#cta-container')
- * .html('Clear all ');
- *
- * containerNode
- * .find('#cta-container > a')
- * .on('click', function(event) {
- * event.preventDefault();
- * CurrentRefinedValuesRenderingOptions.clearAllClick();
- * });
- *
- * var list = CurrentRefinedValuesRenderingOptions.refinements.map(function(refinement) {
- * return ''
- * + refinement.computedLabel + ' ' + refinement.count + ' ';
- * });
- *
- * CurrentRefinedValuesRenderingOptions.find('ul').html(list);
- * CurrentRefinedValuesRenderingOptions.find('li > a').each(function(index) {
- * $(this).on('click', function(event) {
- * event.preventDefault();
- *
- * var refinement = CurrentRefinedValuesRenderingOptions.refinements[index];
- * CurrentRefinedValuesRenderingOptions.refine(refinement);
- * });
- * });
- * } else {
- * containerNode.find('#cta-container').html('');
- * containerNode.find('ul').html('');
- * }
- * }
- *
- * // connect `renderFn` to CurrentRefinedValues logic
- * var customCurrentRefinedValues = instantsearch.connectors.connectCurrentRefinedValues(renderFn);
- *
- * // mount widget on the page
- * search.addWidget(
- * customCurrentRefinedValues({
- * containerNode: $('#custom-crv-container'),
- * })
- * );
- */
-export default function connectCurrentRefinedValues(renderFn, unmountFn) {
- checkRendering(renderFn, usage);
-
- return (widgetParams = {}) => {
- const {
- attributes = [],
- onlyListedAttributes = false,
- clearsQuery = false,
- transformItems = items => items,
- } = widgetParams;
-
- const attributesOK =
- isArray(attributes) &&
- reduce(
- attributes,
- (res, val) =>
- res &&
- isPlainObject(val) &&
- isString(val.name) &&
- (isUndefined(val.label) || isString(val.label)) &&
- (isUndefined(val.template) ||
- isString(val.template) ||
- isFunction(val.template)) &&
- (isUndefined(val.transformData) || isFunction(val.transformData)),
- true
- );
-
- const showUsage =
- false ||
- !isArray(attributes) ||
- !attributesOK ||
- !isBoolean(onlyListedAttributes);
-
- if (showUsage) {
- throw new Error(usage);
- }
-
- const attributeNames = map(attributes, attribute => attribute.name);
- const restrictedTo = onlyListedAttributes ? attributeNames : undefined;
-
- const attributesObj = reduce(
- attributes,
- (res, attribute) => {
- res[attribute.name] = attribute;
- return res;
- },
- {}
- );
-
- return {
- init({ helper, createURL, instantSearchInstance }) {
- this._clearRefinementsAndSearch = () => {
- helper
- .setState(
- clearRefinements({
- helper,
- whiteList: restrictedTo,
- clearsQuery,
- })
- )
- .search();
- };
-
- this._createClearAllURL = () =>
- createURL(
- clearRefinements({ helper, whiteList: restrictedTo, clearsQuery })
- );
-
- const refinements = transformItems(
- getFilteredRefinements(
- {},
- helper.state,
- attributeNames,
- onlyListedAttributes,
- clearsQuery
- )
- );
-
- const _createURL = refinement =>
- createURL(clearRefinementFromState(helper.state, refinement));
- const _clearRefinement = refinement =>
- clearRefinement(helper, refinement);
-
- renderFn(
- {
- attributes: attributesObj,
- clearAllClick: this._clearRefinementsAndSearch,
- clearAllURL: this._createClearAllURL(),
- refine: _clearRefinement,
- createURL: _createURL,
- refinements,
- instantSearchInstance,
- widgetParams,
- },
- true
- );
- },
-
- render({ results, helper, state, createURL, instantSearchInstance }) {
- const refinements = transformItems(
- getFilteredRefinements(
- results,
- state,
- attributeNames,
- onlyListedAttributes,
- clearsQuery
- )
- );
-
- const _createURL = refinement =>
- createURL(clearRefinementFromState(helper.state, refinement));
- const _clearRefinement = refinement =>
- clearRefinement(helper, refinement);
-
- renderFn(
- {
- attributes: attributesObj,
- clearAllClick: this._clearRefinementsAndSearch,
- clearAllURL: this._createClearAllURL(),
- refine: _clearRefinement,
- createURL: _createURL,
- refinements,
- instantSearchInstance,
- widgetParams,
- },
- false
- );
- },
-
- dispose() {
- unmountFn();
- },
- };
- };
-}
-
-function getRestrictedIndexForSort(
- attributeNames,
- otherAttributeNames,
- attributeName
-) {
- const idx = attributeNames.indexOf(attributeName);
- if (idx !== -1) {
- return idx;
- }
- return attributeNames.length + otherAttributeNames.indexOf(attributeName);
-}
-
-function compareRefinements(attributeNames, otherAttributeNames, a, b) {
- const idxa = getRestrictedIndexForSort(
- attributeNames,
- otherAttributeNames,
- a.attributeName
- );
- const idxb = getRestrictedIndexForSort(
- attributeNames,
- otherAttributeNames,
- b.attributeName
- );
- if (idxa === idxb) {
- if (a.name === b.name) {
- return 0;
- }
- return a.name < b.name ? -1 : 1;
- }
- return idxa < idxb ? -1 : 1;
-}
-
-function getFilteredRefinements(
- results,
- state,
- attributeNames,
- onlyListedAttributes,
- clearsQuery
-) {
- let refinements = getRefinements(results, state, clearsQuery);
- const otherAttributeNames = reduce(
- refinements,
- (res, refinement) => {
- if (
- attributeNames.indexOf(refinement.attributeName) === -1 &&
- res.indexOf(refinement.attributeName === -1)
- ) {
- res.push(refinement.attributeName);
- }
- return res;
- },
- []
- );
- refinements = refinements.sort(
- compareRefinements.bind(null, attributeNames, otherAttributeNames)
- );
- if (onlyListedAttributes && !isEmpty(attributeNames)) {
- refinements = filter(
- refinements,
- refinement => attributeNames.indexOf(refinement.attributeName) !== -1
- );
- }
- return refinements.map(computeLabel);
-}
-
-function clearRefinementFromState(state, refinement) {
- switch (refinement.type) {
- case 'facet':
- return state.removeFacetRefinement(
- refinement.attributeName,
- refinement.name
- );
- case 'disjunctive':
- return state.removeDisjunctiveFacetRefinement(
- refinement.attributeName,
- refinement.name
- );
- case 'hierarchical':
- return state.clearRefinements(refinement.attributeName);
- case 'exclude':
- return state.removeExcludeRefinement(
- refinement.attributeName,
- refinement.name
- );
- case 'numeric':
- return state.removeNumericRefinement(
- refinement.attributeName,
- refinement.operator,
- refinement.numericValue
- );
- case 'tag':
- return state.removeTagRefinement(refinement.name);
- case 'query':
- return state.setQueryParameter('query', '');
- default:
- throw new Error(
- `clearRefinement: type ${refinement.type} is not handled`
- );
- }
-}
-
-function clearRefinement(helper, refinement) {
- helper.setState(clearRefinementFromState(helper.state, refinement)).search();
-}
-
-function computeLabel(value) {
- // default to `value.name` if no operators
- value.computedLabel = value.name;
-
- if (value.hasOwnProperty('operator') && typeof value.operator === 'string') {
- let displayedOperator = value.operator;
- if (value.operator === '>=') displayedOperator = '≥';
- if (value.operator === '<=') displayedOperator = '≤';
- value.computedLabel = `${displayedOperator} ${value.name}`;
- }
-
- return value;
-}
diff --git a/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.js b/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.js
new file mode 100644
index 0000000000..714a952dbd
--- /dev/null
+++ b/src/connectors/current-refinements/__tests__/connectCurrentRefinements-test.js
@@ -0,0 +1,480 @@
+import jsHelper, { SearchResults } from 'algoliasearch-helper';
+import connectCurrentRefinements from '../connectCurrentRefinements.js';
+
+describe('connectCurrentRefinements', () => {
+ describe('Usage', () => {
+ it('throws if given both `includedAttributes` and `excludedAttributes`', () => {
+ const customCurrentRefinements = connectCurrentRefinements(() => {});
+
+ expect(
+ customCurrentRefinements.bind(null, {
+ includedAttributes: ['query'],
+ excludedAttributes: ['brand'],
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\`includedAttributes\` and \`excludedAttributes\` cannot be used together."`
+ );
+ });
+ });
+
+ describe('Lifecycle', () => {
+ it('renders during init and render', () => {
+ const helper = jsHelper({});
+ helper.search = () => {};
+ // test that the dummyRendering is called with the isFirstRendering
+ // flag set accordingly
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+ const widget = customCurrentRefinements({
+ foo: 'bar', // dummy param to test `widgetParams`
+ });
+
+ expect(widget.getConfiguration).toBe(undefined);
+ // test if widget is not rendered yet at this point
+ expect(rendering).toHaveBeenCalledTimes(0);
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ // test that rendering has been called during init with isFirstRendering = true
+ expect(rendering).toHaveBeenCalledTimes(1);
+ // test if isFirstRendering is true during init
+ expect(rendering.mock.calls[0][1]).toBe(true);
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.items).toEqual([]);
+ expect(firstRenderingOptions.widgetParams).toEqual({
+ foo: 'bar',
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ // test that rendering has been called during init with isFirstRendering = false
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering.mock.calls[1][1]).toBe(false);
+
+ const secondRenderingOptions = rendering.mock.calls[0][0];
+ expect(secondRenderingOptions.items).toEqual([]);
+ expect(secondRenderingOptions.widgetParams).toEqual({
+ foo: 'bar',
+ });
+ });
+ });
+
+ describe('Widget options', () => {
+ let helper;
+
+ beforeEach(() => {
+ helper = jsHelper({}, '', {
+ facets: ['facet1', 'facet2', 'facet3'],
+ });
+ helper.search = () => {};
+ });
+
+ it('includes all attributes by default except the query', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({});
+
+ helper
+ .addFacetRefinement('facet1', 'facetValue1')
+ .addFacetRefinement('facet2', 'facetValue2')
+ .addFacetRefinement('facet3', 'facetValue3')
+ .setQuery('query');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ attribute: 'facet1',
+ }),
+ expect.objectContaining({
+ attribute: 'facet2',
+ }),
+ expect.objectContaining({
+ attribute: 'facet3',
+ }),
+ ])
+ );
+ expect(rendering.mock.calls[0][0].items).toEqual(
+ expect.not.arrayContaining([
+ expect.objectContaining({
+ attribute: 'query',
+ }),
+ ])
+ );
+ });
+
+ it('includes only the `includedAttributes`', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ includedAttributes: ['facet1', 'query'],
+ });
+
+ helper
+ .addFacetRefinement('facet1', 'facetValue1')
+ .addFacetRefinement('facet2', 'facetValue2')
+ .setQuery('query');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual([
+ expect.objectContaining({
+ attribute: 'facet1',
+ }),
+ expect.objectContaining({
+ attribute: 'query',
+ }),
+ ]);
+ });
+
+ it('does not include query if empty', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ includedAttributes: ['query'],
+ });
+
+ helper.setQuery('');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual([]);
+ });
+
+ it('does not include query if whitespaces', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ includedAttributes: ['query'],
+ });
+
+ helper.setQuery(' ');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual([]);
+ });
+
+ it('excludes the `excludedAttributes` (and overrides the default ["query"])', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ excludedAttributes: ['facet2'],
+ });
+
+ helper
+ .addFacetRefinement('facet1', 'facetValue1')
+ .addFacetRefinement('facet2', 'facetValue2')
+ .setQuery('query');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual([
+ expect.objectContaining({
+ attribute: 'facet1',
+ }),
+ expect.objectContaining({
+ attribute: 'query',
+ }),
+ ]);
+ });
+
+ it('transformItems is applied', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ transformed: true,
+ })),
+ });
+
+ helper
+ .addFacetRefinement('facet1', 'facetValue1')
+ .addFacetRefinement('facet2', 'facetValue2')
+ .addFacetRefinement('facet3', 'facetValue3');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ attribute: 'facet1',
+ transformed: true,
+ }),
+ expect.objectContaining({
+ attribute: 'facet2',
+ transformed: true,
+ }),
+ expect.objectContaining({
+ attribute: 'facet3',
+ transformed: true,
+ }),
+ ])
+ );
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ attribute: 'facet1',
+ transformed: true,
+ }),
+ expect.objectContaining({
+ attribute: 'facet2',
+ transformed: true,
+ }),
+ expect.objectContaining({
+ attribute: 'facet3',
+ transformed: true,
+ }),
+ ])
+ );
+ });
+
+ it('sort numeric refinements by numeric value', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+
+ const widget = customCurrentRefinements({
+ includedAttributes: ['price'],
+ });
+
+ // If sorted alphabetically, "≤ 500" is lower than "≥" so 500 should appear before 100.
+ // However, we want 100 to appear before 500.
+ helper
+ .addNumericRefinement('price', '<=', 500)
+ .addNumericRefinement('price', '>=', 100);
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering.mock.calls[0][0].items).toEqual([
+ expect.objectContaining({
+ attribute: 'price',
+ refinements: [
+ {
+ attribute: 'price',
+ label: '≥ 100',
+ operator: '>=',
+ type: 'numeric',
+ value: 100,
+ },
+ {
+ attribute: 'price',
+ label: '≤ 500',
+ operator: '<=',
+ type: 'numeric',
+ value: 500,
+ },
+ ],
+ }),
+ ]);
+ });
+ });
+
+ describe('Rendering options', () => {
+ let helper;
+
+ beforeEach(() => {
+ helper = jsHelper({}, '', {
+ facets: ['facet1', 'facet2', 'facet3'],
+ });
+ helper.search = () => {};
+ });
+
+ it('provides a `refine` function', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+ const widget = customCurrentRefinements();
+
+ helper.addFacetRefinement('facet1', 'facetValue');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ const [item] = firstRenderingOptions.items;
+ expect(typeof firstRenderingOptions.refine).toBe('function');
+
+ firstRenderingOptions.refine(item.refinements[0]);
+ expect(helper.hasRefinements('facet1')).toBe(false);
+
+ helper.addFacetRefinement('facet1', 'facetValue');
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ const [otherItem] = secondRenderingOptions.items;
+ expect(typeof secondRenderingOptions.refine).toBe('function');
+
+ secondRenderingOptions.refine(otherItem.refinements[0]);
+ expect(helper.hasRefinements('facet1')).toBe(false);
+ });
+
+ it('provides a `createURL` function', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+ const widget = customCurrentRefinements({});
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(typeof firstRenderingOptions.createURL).toBe('function');
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(typeof secondRenderingOptions.createURL).toBe('function');
+ });
+
+ it('provides the refinements', () => {
+ const rendering = jest.fn();
+ const customCurrentRefinements = connectCurrentRefinements(rendering);
+ const widget = customCurrentRefinements({});
+
+ helper.addFacetRefinement('facet1', 'facetValue');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const firstRenderingOptions = rendering.mock.calls[0][0];
+ expect(firstRenderingOptions.items).toEqual([
+ expect.objectContaining({
+ attribute: 'facet1',
+ }),
+ ]);
+
+ helper
+ .addFacetRefinement('facet1', 'facetValue')
+ .addFacetRefinement('facet2', 'facetValue');
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ expect(secondRenderingOptions.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ attribute: 'facet1',
+ }),
+ expect.objectContaining({
+ attribute: 'facet2',
+ }),
+ ])
+ );
+ });
+ });
+});
diff --git a/src/connectors/current-refinements/connectCurrentRefinements.js b/src/connectors/current-refinements/connectCurrentRefinements.js
new file mode 100644
index 0000000000..ff26c4d283
--- /dev/null
+++ b/src/connectors/current-refinements/connectCurrentRefinements.js
@@ -0,0 +1,294 @@
+import { getRefinements, checkRendering } from '../../lib/utils.js';
+
+const usage = `Usage:
+var customCurrentRefinements = connectCurrentRefinements(function renderFn(params, isFirstRendering) {
+ // params = {
+ // items,
+ // refine,
+ // createURL,
+ // instantSearchInstance,
+ // widgetParams,
+ // }
+});
+search.addWidget(
+ customCurrentRefinements({
+ [ includedAttributes ],
+ [ excludedAttributes = ['query'] ],
+ [ transformItems ],
+ })
+);
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectCurrentRefinements.html
+`;
+
+/**
+ * @typedef {Object} Refinement
+ * @property {"facet"|"exclude"|"disjunctive"|"hierarchical"|"numeric"|"query"} type The type of the refinement
+ * @property {string} attribute The attribute on which the refinement is applied
+ * @property {string} label The label of the refinement to display
+ * @property {string} value The raw value of the refinement
+ * @property {string} [operator] The value of the operator, only if applicable
+ * @property {boolean} [exhaustive] Whether the count is exhaustive, only if applicable
+ * @property {number} [count] number of items found, if applicable
+ */
+
+/**
+ * @typedef {Object} RefinementItem
+ * @property {string} attribute The attribute on which the refinement is applied
+ * @property {function} refine The function to remove the refinement
+ * @property {Refinement[]} refinements The current refinements
+ */
+
+/**
+ * @typedef {Object} CurrentRefinementsRenderingOptions
+ * @property {function(item)} refine Clears a single refinement
+ * @property {function(item): string} createURL Creates an individual URL where a single refinement is cleared
+ * @property {RefinementItem[]} items All the refinement items
+ * @property {Object} widgetParams All original `CustomCurrentRefinementsWidgetOptions` forwarded to the `renderFn`.
+ */
+
+/**
+ * @typedef {Object} CustomCurrentRefinementsWidgetOptions
+ * @property {string[]} [includedAttributes] The attributes to include in the refinements (all by default). Cannot be used with `excludedAttributes`.
+ * @property {string[]} [excludedAttributes = ["query"]] The attributes to exclude from the refinements. Cannot be used with `includedAttributes`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * **CurrentRefinements** connector provides the logic to build a widget that will give
+ * the user the ability to see all the currently applied filters and, remove some or all of
+ * them.
+ *
+ * This provides a `refine(item)` function to remove a selected refinement.
+ * Those functions can see their behaviour change based on the widget options used.
+ * @type {Connector}
+ * @param {function(CurrentRefinementsRenderingOptions)} renderFn Rendering function for the custom **CurrentRefinements** widget.
+ * @param {function} unmountFn Unmount function called when the widget is disposed.
+ * @return {function(CustomCurrentRefinementsWidgetOptions)} Re-usable widget factory for a custom **CurrentRefinements** widget.
+ * @example
+ * // custom `renderFn` to render the custom ClearRefinements widget
+ * function renderFn(currentRefinementsRenderingOptions, isFirstRendering) {
+ * var containerNode = currentRefinementsRenderingOptions.widgetParams.containerNode;
+ * if (isFirstRendering) {
+ * containerNode
+ * .html('
');
+ * }
+ *
+ * containerNode
+ * .find('#cta-container > a')
+ * .off('click');
+ *
+ * containerNode
+ * .find('li > a')
+ * .each(function() { $(this).off('click') });
+ *
+ * if (currentRefinementsRenderingOptions.items
+ * && currentRefinementsRenderingOptions.items.length > 0) {
+ * var list = currentRefinementsRenderingOptions.items.map(function(item) {
+ * return '' + item.attribute +
+ * ''
+ * ' ';
+ * });
+ *
+ * currentRefinementsRenderingOptions.find('ul').html(list);
+ * } else {
+ * containerNode.find('#cta-container').html('');
+ * containerNode.find('ul').html('');
+ * }
+ * }
+ *
+ * // connect `renderFn` to CurrentRefinements logic
+ * var customCurrentRefinements = instantsearch.connectors.connectCurrentRefinements(renderFn);
+ *
+ * // mount widget on the page
+ * search.addWidget(
+ * customCurrentRefinements({
+ * containerNode: $('#custom-crv-container'),
+ * })
+ * );
+ */
+export default function connectCurrentRefinements(renderFn, unmountFn) {
+ checkRendering(renderFn, usage);
+
+ return (widgetParams = {}) => {
+ if (widgetParams.includedAttributes && widgetParams.excludedAttributes) {
+ throw new Error(
+ '`includedAttributes` and `excludedAttributes` cannot be used together.'
+ );
+ }
+
+ const {
+ includedAttributes,
+ excludedAttributes = ['query'],
+ transformItems = items => items,
+ } = widgetParams;
+
+ return {
+ init({ helper, createURL, instantSearchInstance }) {
+ const items = transformItems(
+ getFilteredRefinements({
+ results: {},
+ state: helper.state,
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ })
+ );
+
+ renderFn(
+ {
+ items,
+ refine: refinement => clearRefinement(helper, refinement),
+ createURL: refinement =>
+ createURL(clearRefinementFromState(helper.state, refinement)),
+ instantSearchInstance,
+ widgetParams,
+ },
+ true
+ );
+ },
+
+ render({ results, helper, state, createURL, instantSearchInstance }) {
+ const items = transformItems(
+ getFilteredRefinements({
+ results,
+ state,
+ helper,
+ includedAttributes,
+ excludedAttributes,
+ })
+ );
+
+ renderFn(
+ {
+ items,
+ refine: refinement => clearRefinement(helper, refinement),
+ createURL: refinement =>
+ createURL(clearRefinementFromState(helper.state, refinement)),
+ instantSearchInstance,
+ widgetParams,
+ },
+ false
+ );
+ },
+
+ dispose() {
+ unmountFn();
+ },
+ };
+ };
+}
+
+function getFilteredRefinements({
+ results,
+ state,
+ helper,
+ includedAttributes,
+ excludedAttributes,
+}) {
+ const clearsQuery =
+ (includedAttributes || []).indexOf('query') !== -1 ||
+ (excludedAttributes || []).indexOf('query') === -1;
+
+ const filterFunction = includedAttributes
+ ? item => includedAttributes.indexOf(item.attributeName) !== -1
+ : item => excludedAttributes.indexOf(item.attributeName) === -1;
+
+ const items = getRefinements(results, state, clearsQuery)
+ .filter(filterFunction)
+ .map(normalizeRefinement);
+
+ return groupItemsByRefinements(items, helper);
+}
+
+function clearRefinementFromState(state, refinement) {
+ switch (refinement.type) {
+ case 'facet':
+ return state.removeFacetRefinement(
+ refinement.attribute,
+ refinement.value
+ );
+ case 'disjunctive':
+ return state.removeDisjunctiveFacetRefinement(
+ refinement.attribute,
+ refinement.value
+ );
+ case 'hierarchical':
+ return state.removeHierarchicalFacetRefinement(refinement.attribute);
+ case 'exclude':
+ return state.removeExcludeRefinement(
+ refinement.attribute,
+ refinement.value
+ );
+ case 'numeric':
+ return state.removeNumericRefinement(
+ refinement.attribute,
+ refinement.operator,
+ refinement.value
+ );
+ case 'tag':
+ return state.removeTagRefinement(refinement.value);
+ case 'query':
+ return state.setQueryParameter('query', '');
+ default:
+ throw new Error(
+ `clearRefinement: type ${refinement.type} is not handled`
+ );
+ }
+}
+
+function clearRefinement(helper, refinement) {
+ helper.setState(clearRefinementFromState(helper.state, refinement)).search();
+}
+
+function getOperatorSymbol(operator) {
+ switch (operator) {
+ case '>=':
+ return '≥';
+ case '<=':
+ return '≤';
+ default:
+ return operator;
+ }
+}
+
+function normalizeRefinement(refinement) {
+ const value =
+ refinement.type === 'numeric' ? Number(refinement.name) : refinement.name;
+ const label = refinement.operator
+ ? `${getOperatorSymbol(refinement.operator)} ${refinement.name}`
+ : refinement.name;
+
+ return {
+ attribute: refinement.attributeName,
+ type: refinement.type,
+ value,
+ label,
+ ...(refinement.operator !== undefined && { operator: refinement.operator }),
+ ...(refinement.count !== undefined && { count: refinement.count }),
+ ...(refinement.exhaustive !== undefined && {
+ exhaustive: refinement.exhaustive,
+ }),
+ };
+}
+
+function groupItemsByRefinements(items, helper) {
+ return items.reduce(
+ (results, currentItem) => [
+ ...results.filter(result => result.attribute !== currentItem.attribute),
+ {
+ attribute: currentItem.attribute,
+ refinements: items
+ .filter(result => result.attribute === currentItem.attribute)
+ // We want to keep the order of refinements except the numeric ones.
+ .sort((a, b) => (a.type === 'numeric' ? a.value - b.value : 0)),
+ refine: refinement => clearRefinement(helper, refinement),
+ },
+ ],
+ []
+ );
+}
diff --git a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
index 3eafaa21be..d153435436 100644
--- a/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
+++ b/src/connectors/geo-search/__tests__/connectGeoSearch-test.js
@@ -1,23 +1,43 @@
import last from 'lodash/last';
import first from 'lodash/first';
import algoliasearchHelper, {
- SearchResults,
SearchParameters,
+ SearchResults,
} from 'algoliasearch-helper';
import connectGeoSearch from '../connectGeoSearch';
-const createFakeHelper = client => {
- const helper = algoliasearchHelper(client);
+describe('connectGeoSearch', () => {
+ const createFakeHelper = client => {
+ const helper = algoliasearchHelper(client);
+
+ helper.search = jest.fn();
+
+ return helper;
+ };
+
+ const getInitializedWidget = () => {
+ const render = jest.fn();
+ const makeWidget = connectGeoSearch(render);
+
+ const widget = makeWidget();
+
+ const helper = createFakeHelper({});
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
- helper.search = jest.fn();
+ const { refine } = render.mock.calls[0][0];
- return helper;
-};
+ return [widget, helper, refine];
+ };
-const firstRenderArgs = fn => first(fn.mock.calls)[0];
-const lastRenderArgs = fn => last(fn.mock.calls)[0];
+ const firstRenderArgs = fn => first(fn.mock.calls)[0];
+ const lastRenderArgs = fn => last(fn.mock.calls)[0];
-describe('connectGeoSearch - rendering', () => {
it('expect to be a widget', () => {
const render = jest.fn();
const unmount = jest.fn();
@@ -26,10 +46,11 @@ describe('connectGeoSearch - rendering', () => {
const widget = customGeoSearch();
expect(widget).toEqual({
- getConfiguration: expect.any(Function),
init: expect.any(Function),
render: expect.any(Function),
dispose: expect.any(Function),
+ getWidgetState: expect.any(Function),
+ getWidgetSearchParameters: expect.any(Function),
});
});
@@ -217,18 +238,12 @@ describe('connectGeoSearch - rendering', () => {
);
});
- it('expect to render with position from the state', () => {
+ it('expect to render with position', () => {
const render = jest.fn();
const unmount = jest.fn();
const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 10,
- lng: 12,
- },
- });
-
+ const widget = customGeoSearch();
const helper = createFakeHelper({});
// Simulate the configuration or external setter
@@ -294,66 +309,6 @@ describe('connectGeoSearch - rendering', () => {
);
});
- it('expect to render with insideBoundingBox from the state', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 10,
- lng: 12,
- },
- });
-
- const helper = createFakeHelper({});
-
- // Simulate the configuration or external setter
- helper.setQueryParameter('insideBoundingBox', [
- [
- 48.84174222399724,
- 2.367719162523599,
- 48.81614630305218,
- 2.284205902635904,
- ],
- ]);
-
- widget.init({
- helper,
- state: helper.state,
- });
-
- expect(render).toHaveBeenCalledTimes(1);
- expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
-
- widget.render({
- results: new SearchResults(helper.getState(), [
- {
- hits: [],
- },
- ]),
- helper,
- });
-
- expect(render).toHaveBeenCalledTimes(2);
- expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
-
- // Simulate the configuration or external setter
- helper.setQueryParameter('insideBoundingBox');
-
- widget.render({
- results: new SearchResults(helper.getState(), [
- {
- hits: [],
- },
- ]),
- helper,
- });
-
- expect(render).toHaveBeenCalledTimes(3);
- expect(lastRenderArgs(render).isRefinedWithMap()).toBe(false);
- });
-
it('expect to reset the map state when position changed', () => {
const render = jest.fn();
const unmount = jest.fn();
@@ -614,6 +569,141 @@ describe('connectGeoSearch - rendering', () => {
expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
});
+ describe('currentRefinement', () => {
+ it('expect to render with currentRefinement from a string', () => {
+ const render = jest.fn();
+ const unmount = jest.fn();
+
+ const customGeoSearch = connectGeoSearch(render, unmount);
+ const widget = customGeoSearch();
+ const helper = createFakeHelper({});
+
+ // Simulate the configuration or external setter (like URLSync)
+ helper.setQueryParameter('insideBoundingBox', '10,12,12,14');
+
+ widget.init({
+ helper,
+ state: helper.state,
+ });
+
+ expect(render).toHaveBeenCalledTimes(1);
+ expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
+ expect(lastRenderArgs(render).currentRefinement).toEqual({
+ northEast: {
+ lat: 10,
+ lng: 12,
+ },
+ southWest: {
+ lat: 12,
+ lng: 14,
+ },
+ });
+
+ widget.render({
+ results: new SearchResults(helper.getState(), [
+ {
+ hits: [],
+ },
+ ]),
+ helper,
+ });
+
+ expect(render).toHaveBeenCalledTimes(2);
+ expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
+ expect(lastRenderArgs(render).currentRefinement).toEqual({
+ northEast: {
+ lat: 10,
+ lng: 12,
+ },
+ southWest: {
+ lat: 12,
+ lng: 14,
+ },
+ });
+
+ // Simulate the configuration or external setter (like URLSync)
+ helper.setQueryParameter('insideBoundingBox');
+
+ widget.render({
+ results: new SearchResults(helper.getState(), [
+ {
+ hits: [],
+ },
+ ]),
+ helper,
+ });
+
+ expect(render).toHaveBeenCalledTimes(3);
+ expect(lastRenderArgs(render).currentRefinement).toBeUndefined();
+ });
+
+ it('expect to render with currentRefinement from an array', () => {
+ const render = jest.fn();
+ const unmount = jest.fn();
+
+ const customGeoSearch = connectGeoSearch(render, unmount);
+ const widget = customGeoSearch();
+ const helper = createFakeHelper({});
+
+ helper.setQueryParameter('insideBoundingBox', [[10, 12, 12, 14]]);
+
+ widget.init({
+ helper,
+ state: helper.state,
+ });
+
+ expect(render).toHaveBeenCalledTimes(1);
+ expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
+ expect(lastRenderArgs(render).currentRefinement).toEqual({
+ northEast: {
+ lat: 10,
+ lng: 12,
+ },
+ southWest: {
+ lat: 12,
+ lng: 14,
+ },
+ });
+
+ widget.render({
+ results: new SearchResults(helper.getState(), [
+ {
+ hits: [],
+ },
+ ]),
+ helper,
+ });
+
+ expect(render).toHaveBeenCalledTimes(2);
+ expect(lastRenderArgs(render).isRefinedWithMap()).toBe(true);
+ expect(lastRenderArgs(render).currentRefinement).toEqual({
+ northEast: {
+ lat: 10,
+ lng: 12,
+ },
+ southWest: {
+ lat: 12,
+ lng: 14,
+ },
+ });
+
+ // Simulate the configuration or external setter (like URLSync)
+ helper.setQueryParameter('insideBoundingBox');
+
+ widget.render({
+ results: new SearchResults(helper.getState(), [
+ {
+ hits: [],
+ },
+ ]),
+ helper,
+ });
+
+ expect(render).toHaveBeenCalledTimes(3);
+ expect(lastRenderArgs(render).currentRefinement).toBeUndefined();
+ });
+ });
+
describe('refine', () => {
it('expect to refine with the given bounds during init', () => {
const render = jest.fn();
@@ -1022,548 +1112,152 @@ describe('connectGeoSearch - rendering', () => {
);
});
});
-});
-describe('connectGeoSearch - getConfiguration', () => {
- describe('aroundLatLngViaIP', () => {
- it('expect to set aroundLatLngViaIP', () => {
+ describe('dispose', () => {
+ it('expect reset insideBoundingBox', () => {
const render = jest.fn();
const unmount = jest.fn();
const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: true,
- });
-
- const expectation = {
- aroundLatLngViaIP: true,
- };
-
- const actual = widget.getConfiguration(new SearchParameters());
-
- expect(actual).toEqual(expectation);
- });
-
- it('expect to not set aroundLatLngViaIP when position is given', () => {
- const render = jest.fn();
- const unmount = jest.fn();
+ const widget = customGeoSearch();
+ const helper = createFakeHelper({});
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: true,
- position: {
- lat: 12,
- lng: 10,
- },
- });
+ helper.setQueryParameter('insideBoundingBox', '10,12,12,14');
const expectation = {
- aroundLatLng: '12, 10',
+ insideBoundingBox: undefined,
};
- const actual = widget.getConfiguration(new SearchParameters());
+ const actual = widget.dispose({ state: helper.getState() });
- expect(actual).toEqual(expectation);
+ expect(unmount).toHaveBeenCalled();
+ expect(actual).toMatchObject(expectation);
});
+ });
- it("expect to not set aroundLatLngViaIP when it's already set", () => {
- const render = jest.fn();
- const unmount = jest.fn();
+ describe('getWidgetState', () => {
+ it('expect to return the uiState unmodified if no boundingBox is selected', () => {
+ const [widget, helper] = getInitializedWidget();
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: true,
+ const uiStateBefore = {};
+ const uiStateAfter = widget.getWidgetState(uiStateBefore, {
+ searchParameters: helper.state,
+ helper,
});
- const expectation = {};
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundLatLngViaIP: false,
- })
- );
-
- expect(actual).toEqual(expectation);
+ expect(uiStateAfter).toBe(uiStateBefore);
});
- it('expect to not set aroundLatLngViaIP when aroundLatLng is already set', () => {
- const render = jest.fn();
- const unmount = jest.fn();
+ it('expect to return the uiState with an entry equal to the boundingBox', () => {
+ const [widget, helper, refine] = getInitializedWidget();
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: true,
+ refine({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
});
- const expectation = {};
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundLatLng: '10, 12',
- })
- );
-
- expect(actual).toEqual(expectation);
- });
- });
-
- describe('aroundLatLng', () => {
- it('expect to set aroundLatLng', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 12,
- lng: 10,
- },
+ const uiStateBefore = {};
+ const uiStateAfter = widget.getWidgetState(uiStateBefore, {
+ searchParameters: helper.state,
+ helper,
});
- const expectation = {
- aroundLatLng: '12, 10',
- };
-
- const actual = widget.getConfiguration(new SearchParameters());
-
- expect(actual).toEqual(expectation);
- });
-
- it('expect to set aroundLatLng when aroundLatLngViaIP is already set to false', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 12,
- lng: 10,
+ expect(uiStateAfter).toEqual({
+ geoSearch: {
+ boundingBox: '10,12,12,14',
},
});
-
- const expectation = {
- aroundLatLng: '12, 10',
- };
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundLatLngViaIP: false,
- })
- );
-
- expect(actual).toEqual(expectation);
});
- it("expect to not set aroundLatLng when it's already set", () => {
- const render = jest.fn();
- const unmount = jest.fn();
+ it('expect to return the same uiState instance if the value is alreay there', () => {
+ const [widget, helper, refine] = getInitializedWidget();
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 12,
- lng: 10,
- },
+ refine({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
});
- const expectation = {};
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundLatLng: '12, 12',
- })
+ const uiStateBefore = widget.getWidgetState(
+ {},
+ {
+ searchParameters: helper.state,
+ helper,
+ }
);
- expect(actual).toEqual(expectation);
- });
-
- it('expect to not set aroundLatLng when aroundLatLngViaIP is already set to true', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 12,
- lng: 10,
- },
+ const uiStateAfter = widget.getWidgetState(uiStateBefore, {
+ searchParameters: helper.state,
+ helper,
});
- const expectation = {};
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundLatLngViaIP: true,
- })
- );
-
- expect(actual).toEqual(expectation);
+ expect(uiStateAfter).toBe(uiStateBefore);
});
});
- describe('aroundRadius', () => {
- it('expect to set aroundRadius', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- radius: 1000,
- });
-
- const expectation = {
- aroundLatLngViaIP: true,
- aroundRadius: 1000,
- };
-
- const actual = widget.getConfiguration(new SearchParameters());
-
- expect(actual).toEqual(expectation);
- });
-
- it("expect to not set aroundRadius when it's already defined", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- radius: 1000,
- });
-
- const expectation = {
- aroundLatLngViaIP: true,
- };
+ describe('getWidgetSearchParameters', () => {
+ it('expect to return the same SearchParameters if the uiState is empty', () => {
+ const [widget, helper] = getInitializedWidget();
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundRadius: 500,
- })
+ const uiState = {};
+ const searchParametersBefore = SearchParameters.make(helper.state);
+ const searchParametersAfter = widget.getWidgetSearchParameters(
+ searchParametersBefore,
+ { uiState }
);
- expect(actual).toEqual(expectation);
+ expect(searchParametersAfter).toBe(searchParametersBefore);
});
- });
-
- describe('aroundPrecision', () => {
- it('expect to set aroundPrecision', () => {
- const render = jest.fn();
- const unmount = jest.fn();
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- precision: 1000,
- });
+ it('expect to return the same SearchParameters if the geoSearch attribute is empty', () => {
+ const [widget, helper] = getInitializedWidget();
- const expectation = {
- aroundLatLngViaIP: true,
- aroundPrecision: 1000,
+ const uiState = {
+ geoSearch: {},
};
- const actual = widget.getConfiguration(new SearchParameters());
-
- expect(actual).toEqual(expectation);
- });
-
- it("expect to not set aroundPrecision when it's already defined", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- precision: 1000,
- });
-
- const expectation = {
- aroundLatLngViaIP: true,
- };
-
- const actual = widget.getConfiguration(
- new SearchParameters({
- aroundPrecision: 500,
- })
+ const searchParametersBefore = SearchParameters.make(helper.state);
+ const searchParametersAfter = widget.getWidgetSearchParameters(
+ searchParametersBefore,
+ { uiState }
);
- expect(actual).toEqual(expectation);
- });
- });
-});
-
-describe('connectGeoSearch - dispose', () => {
- it('expect reset insideBoundingBox', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: false,
- });
-
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('insideBoundingBox', '10,12,12,14');
-
- const expectation = {
- insideBoundingBox: undefined,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
-
- it('expect reset multiple parameters', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- radius: 100,
- precision: 25,
- position: {
- lat: 10,
- lng: 12,
- },
- });
-
- const helper = createFakeHelper({});
-
- helper.setState(widget.getConfiguration(new SearchParameters()));
-
- const expectation = {
- aroundRadius: undefined,
- aroundPrecision: undefined,
- aroundLatLng: undefined,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
-
- describe('aroundLatLngViaIP', () => {
- it('expect reset aroundLatLngViaIP', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch();
-
- const helper = createFakeHelper({});
-
- helper.setState(widget.getConfiguration(new SearchParameters()));
-
- const expectation = {
- aroundLatLngViaIP: undefined,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
+ expect(searchParametersAfter).toBe(searchParametersBefore);
});
- it("expect to not reset aroundLatLngViaIP when it's not set by the widget", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- enableGeolocationWithIP: false,
- });
+ it('expect to return the SearchParameters with the boundingBox value from the uiState', () => {
+ const [widget, helper] = getInitializedWidget();
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('aroundLatLngViaIP', true);
-
- const expectation = {
- aroundLatLngViaIP: true,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
-
- it('expect to not reset aroundLatLngViaIP when position is given', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 10,
- lng: 12,
+ const uiState = {
+ geoSearch: {
+ boundingBox: '10,12,12,14',
},
- });
-
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('aroundLatLngViaIP', true);
-
- const expectation = {
- aroundLatLngViaIP: true,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
- });
-
- describe('aroundLatLng', () => {
- it('expect to reset aroundLatLng', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- position: {
- lat: 10,
- lng: 12,
- },
- });
-
- const helper = createFakeHelper({});
-
- helper.setState(widget.getConfiguration(new SearchParameters()));
-
- const expectation = {
- aroundLatLng: undefined,
};
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
-
- it("expect to not reset aroundLatLng when it's not set by the widget", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch();
-
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('aroundLatLng', '10, 12');
-
- const expectation = {
- aroundLatLng: '10, 12',
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
- });
-
- describe('aroundRadius', () => {
- it('expect to reset aroundRadius', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- radius: 1000,
- });
-
- const helper = createFakeHelper({});
-
- helper.setState(widget.getConfiguration(new SearchParameters()));
-
- const expectation = {
- aroundRadius: undefined,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
+ const searchParametersBefore = SearchParameters.make(helper.state);
+ const searchParametersAfter = widget.getWidgetSearchParameters(
+ searchParametersBefore,
+ { uiState }
+ );
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
+ expect(searchParametersAfter.insideBoundingBox).toBe('10,12,12,14');
});
- it("expect to not reset aroundRadius when it's not set by the widget", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch();
+ it('expect to remove the boundingBox from the SearchParameters if the value is not in the uiState', () => {
+ const [widget, helper, refine] = getInitializedWidget();
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('aroundRadius', 1000);
-
- const expectation = {
- aroundRadius: 1000,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
- });
-
- describe('aroundPrecision', () => {
- it('expect to reset aroundPrecision', () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch({
- precision: 1000,
+ refine({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
});
- const helper = createFakeHelper({});
-
- helper.setState(widget.getConfiguration(new SearchParameters()));
-
- const expectation = {
- aroundPrecision: undefined,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
-
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
- });
-
- it("expect to not reset aroundPrecision when it's not set by the widget", () => {
- const render = jest.fn();
- const unmount = jest.fn();
-
- const customGeoSearch = connectGeoSearch(render, unmount);
- const widget = customGeoSearch();
-
- const helper = createFakeHelper({});
-
- helper
- .setState(widget.getConfiguration(new SearchParameters()))
- .setQueryParameter('aroundPrecision', 1000);
-
- const expectation = {
- aroundPrecision: 1000,
- };
-
- const actual = widget.dispose({ state: helper.getState() });
+ const uiState = {};
+ const searchParametersBefore = SearchParameters.make(helper.state);
+ const searchParametersAfter = widget.getWidgetSearchParameters(
+ searchParametersBefore,
+ { uiState }
+ );
- expect(unmount).toHaveBeenCalled();
- expect(actual).toMatchObject(expectation);
+ expect(searchParametersAfter.insideBoundingBox).toBeUndefined();
});
});
});
diff --git a/src/connectors/geo-search/connectGeoSearch.js b/src/connectors/geo-search/connectGeoSearch.js
index b852482ccd..a6fdcdbf3c 100644
--- a/src/connectors/geo-search/connectGeoSearch.js
+++ b/src/connectors/geo-search/connectGeoSearch.js
@@ -1,5 +1,10 @@
import noop from 'lodash/noop';
-import { checkRendering, parseAroundLatLngFromString } from '../../lib/utils';
+import {
+ checkRendering,
+ warn,
+ aroundLatLngToPosition,
+ insideBoundingBoxToBoundingBox,
+} from '../../lib/utils';
const usage = `Usage:
@@ -7,6 +12,7 @@ var customGeoSearch = connectGeoSearch(function render(params, isFirstRendering)
// params = {
// items,
// position,
+ // currentRefinement,
// refine,
// clearMapRefinement,
// isRefinedWithMap,
@@ -23,10 +29,6 @@ var customGeoSearch = connectGeoSearch(function render(params, isFirstRendering)
search.addWidget(
customGeoSearch({
[ enableRefineOnMapMove = true ],
- [ enableGeolocationWithIP = true ],
- [ position ],
- [ radius ],
- [ precision ],
[ transformItems ],
})
);
@@ -49,19 +51,14 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomGeoSearchWidgetOptions
* @property {boolean} [enableRefineOnMapMove=true] If true, refine will be triggered as you move the map.
- * @property {boolean} [enableGeolocationWithIP=true] If true, the IP will be use for the geolocation. When the `position` option is provided this option will be ignored. See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP) for more information.
- * @property {LatLng} [position] Position that will be use to search around.
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng) for more information.
- * @property {number} [radius] Maximum radius to search around the position (in meters).
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius) for more information.
- * @property {number} [precision] Precision of geo search (in meters).
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision) for more information.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
* @typedef {Object} GeoSearchRenderingOptions
* @property {Object[]} items The matched hits from Algolia API.
+ * @property {LatLng} position The current position of the search.
+ * @property {Bounds} currentRefinement The current bounding box of the search.
* @property {function(Bounds)} refine Sets a bounding box to filter the results from the given map bounds.
* @property {function()} clearMapRefinement Reset the current bounding box refinement.
* @property {function(): boolean} isRefinedWithMap Return true if the current refinement is set with the map bounds.
@@ -135,13 +132,76 @@ const connectGeoSearch = (renderFn, unmountFn) => {
return (widgetParams = {}) => {
const {
enableRefineOnMapMove = true,
- enableGeolocationWithIP = true,
- position,
- radius,
- precision,
transformItems = items => items,
} = widgetParams;
+ // Always trigger this message because the default value was `true`. We can't
+ // display the message only when the parameter is defined otherwise a user that was
+ // relying on the default value won't have any information about the changes.
+ warn(`
+The option \`enableGeolocationWithIP\` has been removed from the GeoSearch widget.
+Please consider using the \`Configure\` widget instead:
+
+search.addWidget(
+ configure({
+ aroundLatLngViaIP: ${widgetParams.enableGeolocationWithIP || 'true'},
+ })
+);
+
+You can find more information inside the migration guide:
+http://community.algolia.com/instantsearch.js/migration-guide
+ `);
+
+ if (typeof widgetParams.position !== 'undefined') {
+ warn(`
+The option \`position\` has been removed from the GeoSearch widget.
+Please consider using the \`Configure\` widget instead:
+
+search.addWidget(
+ configure({
+ aroundLatLng: '${widgetParams.position.lat}, ${widgetParams.position.lng}',
+ })
+);
+
+You can find more information inside the migration guide:
+http://community.algolia.com/instantsearch.js/migration-guide
+ `);
+ }
+
+ if (typeof widgetParams.radius !== 'undefined') {
+ warn(`
+The option \`radius\` has been removed from the GeoSearch widget.
+Please consider using the \`Configure\` widget instead:
+
+search.addWidget(
+ configure({
+ aroundRadius: ${widgetParams.radius},
+ })
+);
+
+You can find more information inside the migration guide:
+
+http://community.algolia.com/instantsearch.js/migration-guide
+ `);
+ }
+
+ if (typeof widgetParams.precision !== 'undefined') {
+ warn(`
+The option \`precision\` has been removed from the GeoSearch widget.
+Please consider using the \`Configure\` widget instead:
+
+search.addWidget(
+ configure({
+ aroundPrecision: ${widgetParams.precision},
+ })
+);
+
+You can find more information inside the migration guide:
+
+http://community.algolia.com/instantsearch.js/migration-guide
+ `);
+ }
+
const widgetState = {
isRefineOnMapMove: enableRefineOnMapMove,
hasMapMoveSinceLastRefine: false,
@@ -152,7 +212,11 @@ const connectGeoSearch = (renderFn, unmountFn) => {
};
const getPositionFromState = state =>
- state.aroundLatLng && parseAroundLatLngFromString(state.aroundLatLng);
+ state.aroundLatLng && aroundLatLngToPosition(state.aroundLatLng);
+
+ const getCurrentRefinementFromState = state =>
+ state.insideBoundingBox &&
+ insideBoundingBoxToBoundingBox(state.insideBoundingBox);
const refine = helper => ({ northEast: ne, southWest: sw }) => {
const boundingBox = [ne.lat, ne.lng, sw.lat, sw.lng].join();
@@ -171,7 +235,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
const toggleRefineOnMapMove = () =>
widgetState.internalToggleRefineOnMapMove();
- const createInternalToggleRefinementonMapMove = (render, args) => () => {
+ const createInternalToggleRefinementOnMapMove = (render, args) => () => {
widgetState.isRefineOnMapMove = !widgetState.isRefineOnMapMove;
render(args);
@@ -199,7 +263,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
const { state, helper, instantSearchInstance } = initArgs;
const isFirstRendering = true;
- widgetState.internalToggleRefineOnMapMove = createInternalToggleRefinementonMapMove(
+ widgetState.internalToggleRefineOnMapMove = createInternalToggleRefinementOnMapMove(
noop,
initArgs
);
@@ -213,6 +277,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
{
items: [],
position: getPositionFromState(state),
+ currentRefinement: getCurrentRefinementFromState(state),
refine: refine(helper),
clearMapRefinement: clearMapRefinement(helper),
isRefinedWithMap: isRefinedWithMap(state),
@@ -251,7 +316,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
widgetState.lastRefinePosition = state.aroundLatLng || '';
widgetState.lastRefineBoundingBox = state.insideBoundingBox || '';
- widgetState.internalToggleRefineOnMapMove = createInternalToggleRefinementonMapMove(
+ widgetState.internalToggleRefineOnMapMove = createInternalToggleRefinementOnMapMove(
render,
renderArgs
);
@@ -267,6 +332,7 @@ const connectGeoSearch = (renderFn, unmountFn) => {
{
items,
position: getPositionFromState(state),
+ currentRefinement: getCurrentRefinementFromState(state),
refine: refine(helper),
clearMapRefinement: clearMapRefinement(helper),
isRefinedWithMap: isRefinedWithMap(state),
@@ -285,57 +351,41 @@ const connectGeoSearch = (renderFn, unmountFn) => {
init,
render,
- getConfiguration(previous) {
- const configuration = {};
-
- if (
- enableGeolocationWithIP &&
- !position &&
- !previous.aroundLatLng &&
- previous.aroundLatLngViaIP === undefined
- ) {
- configuration.aroundLatLngViaIP = true;
- }
-
- if (position && !previous.aroundLatLng && !previous.aroundLatLngViaIP) {
- configuration.aroundLatLng = `${position.lat}, ${position.lng}`;
- }
-
- if (radius && !previous.aroundRadius) {
- configuration.aroundRadius = radius;
- }
-
- if (precision && !previous.aroundPrecision) {
- configuration.aroundPrecision = precision;
- }
-
- return configuration;
- },
-
dispose({ state }) {
unmountFn();
- let nextState = state;
+ return state.setQueryParameter('insideBoundingBox');
+ },
- if (enableGeolocationWithIP && !position) {
- nextState = nextState.setQueryParameter('aroundLatLngViaIP');
- }
+ getWidgetState(uiState, { searchParameters }) {
+ const boundingBox = searchParameters.insideBoundingBox;
- if (position) {
- nextState = nextState.setQueryParameter('aroundLatLng');
+ if (
+ !boundingBox ||
+ (uiState &&
+ uiState.geoSearch &&
+ uiState.geoSearch.boundingBox === boundingBox)
+ ) {
+ return uiState;
}
- if (radius) {
- nextState = nextState.setQueryParameter('aroundRadius');
- }
+ return {
+ ...uiState,
+ geoSearch: {
+ boundingBox,
+ },
+ };
+ },
- if (precision) {
- nextState = nextState.setQueryParameter('aroundPrecision');
+ getWidgetSearchParameters(searchParameters, { uiState }) {
+ if (!uiState || !uiState.geoSearch) {
+ return searchParameters.setQueryParameter('insideBoundingBox');
}
- nextState = nextState.setQueryParameter('insideBoundingBox');
-
- return nextState;
+ return searchParameters.setQueryParameter(
+ 'insideBoundingBox',
+ uiState.geoSearch.boundingBox
+ );
},
};
};
diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
index 2d7c40802b..521e5fcb9d 100644
--- a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
+++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.js
@@ -1,63 +1,161 @@
-import jsHelper from 'algoliasearch-helper';
-const SearchResults = jsHelper.SearchResults;
-const SearchParameters = jsHelper.SearchParameters;
-
+import jsHelper, {
+ SearchResults,
+ SearchParameters,
+} from 'algoliasearch-helper';
import connectHierarchicalMenu from '../connectHierarchicalMenu.js';
describe('connectHierarchicalMenu', () => {
- it('It should compute getConfiguration() correctly', () => {
- const rendering = jest.fn();
- const makeWidget = connectHierarchicalMenu(rendering);
+ describe('getConfiguration', () => {
+ it('It should compute getConfiguration() correctly', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
- const widget = makeWidget({ attributes: ['category', 'sub_category'] });
+ const widget = makeWidget({ attributes: ['category', 'sub_category'] });
+
+ // when there is no hierarchicalFacets into current configuration
+ {
+ const config = widget.getConfiguration({});
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 10,
+ });
+ }
- // when there is no hierarchicalFacets into current configuration
- {
- const config = widget.getConfiguration({});
- expect(config).toEqual({
- hierarchicalFacets: [
- {
- attributes: ['category', 'sub_category'],
- name: 'category',
- rootPath: null,
- separator: ' > ',
- showParentLevel: true,
- },
- ],
- maxValuesPerFacet: 10,
- });
- }
+ // when there is an identical hierarchicalFacets into current configuration
+ {
+ const spy = jest.spyOn(global.console, 'warn');
+ const config = widget.getConfiguration({
+ hierarchicalFacets: [{ name: 'category' }],
+ });
+ expect(config).toEqual({});
+ expect(spy).toHaveBeenCalled();
+ spy.mockReset();
+ spy.mockRestore();
+ }
+
+ // when there is already a different hierarchicalFacets into current configuration
+ {
+ const config = widget.getConfiguration({
+ hierarchicalFacets: [{ name: 'foo' }],
+ });
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 10,
+ });
+ }
+ });
- // when there is an identical hierarchicalFacets into current configuration
- {
- const spy = jest.spyOn(global.console, 'warn');
- const config = widget.getConfiguration({
- hierarchicalFacets: [{ name: 'category' }],
- });
- expect(config).toEqual({});
- expect(spy).toHaveBeenCalled();
- spy.mockReset();
- spy.mockRestore();
- }
-
- // when there is already a different hierarchicalFacets into current configuration
- {
- const config = widget.getConfiguration({
- hierarchicalFacets: [{ name: 'foo' }],
+ it('sets the correct limit with showMore', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
+
+ const widget = makeWidget({
+ attributes: ['category', 'sub_category'],
+ showMore: true,
+ limit: 3,
+ showMoreLimit: 100,
});
- expect(config).toEqual({
- hierarchicalFacets: [
- {
- attributes: ['category', 'sub_category'],
- name: 'category',
- rootPath: null,
- separator: ' > ',
- showParentLevel: true,
- },
- ],
- maxValuesPerFacet: 10,
+
+ // when there is no other limit set
+ {
+ const config = widget.getConfiguration({});
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 100,
+ });
+ }
+
+ // when there is a bigger already limit set
+ {
+ const config = widget.getConfiguration({
+ maxValuesPerFacet: 101,
+ });
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 101,
+ });
+ }
+ });
+
+ it('sets the correct custom limit', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
+
+ const widget = makeWidget({
+ attributes: ['category', 'sub_category'],
+ limit: 3,
+ showMore: true,
+ showMoreLimit: 6,
});
- }
+
+ // when there is no other limit set
+ {
+ const config = widget.getConfiguration({});
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 6,
+ });
+ }
+
+ // when there is a bigger already limit set
+ {
+ const config = widget.getConfiguration({
+ maxValuesPerFacet: 101,
+ });
+ expect(config).toEqual({
+ hierarchicalFacets: [
+ {
+ attributes: ['category', 'sub_category'],
+ name: 'category',
+ rootPath: null,
+ separator: ' > ',
+ showParentLevel: true,
+ },
+ ],
+ maxValuesPerFacet: 101,
+ });
+ }
+ });
});
it('Renders during init and render', () => {
@@ -438,4 +536,222 @@ describe('connectHierarchicalMenu', () => {
});
});
});
+
+ describe('show more', () => {
+ it('can toggle the limits based on the default showMoreLimit value', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
+ const widget = makeWidget({
+ attributes: ['category'],
+ limit: 2,
+ showMore: true,
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ a: 880,
+ b: 880,
+ c: 880,
+ d: 880,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ a: 880,
+ b: 880,
+ c: 880,
+ d: 880,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const { toggleShowMore } = rendering.mock.calls[1][0];
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'a',
+ value: 'a',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'b',
+ value: 'b',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ ],
+ }),
+ expect.anything()
+ );
+
+ toggleShowMore();
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'a',
+ value: 'a',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'b',
+ value: 'b',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'c',
+ value: 'c',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'd',
+ value: 'd',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ ],
+ }),
+ expect.anything()
+ );
+ });
+
+ it('can toggle the limits based on showMoreLimit', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHierarchicalMenu(rendering);
+ const widget = makeWidget({
+ attributes: ['category'],
+ limit: 2,
+ showMore: true,
+ showMoreLimit: 3,
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ a: 880,
+ b: 880,
+ c: 880,
+ d: 880,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ a: 880,
+ b: 880,
+ c: 880,
+ d: 880,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const { toggleShowMore } = rendering.mock.calls[1][0];
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'a',
+ value: 'a',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'b',
+ value: 'b',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ ],
+ }),
+ expect.anything()
+ );
+
+ toggleShowMore();
+
+ expect(rendering).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ items: [
+ {
+ label: 'a',
+ value: 'a',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'b',
+ value: 'b',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ {
+ label: 'c',
+ value: 'c',
+ count: 880,
+ isRefined: false,
+ data: null,
+ },
+ ],
+ }),
+ expect.anything()
+ );
+ });
+ });
});
diff --git a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
index f10f8b92ad..f185407aa5 100644
--- a/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
+++ b/src/connectors/hierarchical-menu/connectHierarchicalMenu.js
@@ -1,6 +1,5 @@
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
-
import { checkRendering, warn } from '../../lib/utils.js';
const usage = `Usage:
@@ -20,6 +19,8 @@ search.addWidget(
[ rootPath = null ],
[ showParentLevel = true ],
[ limit = 10 ],
+ [ showMore = false ],
+ [ showMoreLimit = 20 ],
[ sortBy = ['name:asc'] ],
[ transformItems ],
})
@@ -43,7 +44,9 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @property {string} [rootPath = null] Prefix path to use if the first level is not the root level.
* @property {boolean} [showParentLevel=false] Show the siblings of the selected parent levels of the current refined value. This
* does not impact the root level.
- * @property {number} [limit = 10] Max number of value to display.
+ * @property {number} [limit = 10] Max number of values to display.
+ * @property {boolean} [showMore = false] Whether to display the "show more" button.
+ * @property {number} [showMoreLimit = 20] Max number of values to display when showing more.
* @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
@@ -67,7 +70,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* levels deep.
*
* There's a complete example available on how to write a custom **HierarchicalMenu**:
- * [hierarchicalMenu.js](https://github.com/algolia/instantsearch.js/blob/develop/dev/app/jquery/widgets/hierarchicalMenu.js)
+ * [hierarchicalMenu.js](https://github.com/algolia/instantsearch.js/blob/develop/storybook/app/jquery/widgets/hierarchicalMenu.js)
* @type {Connector}
* @param {function(HierarchicalMenuRenderingOptions)} renderFn Rendering function for the custom **HierarchicalMenu** widget.
* @param {function} unmountFn Unmount function called when the widget is disposed.
@@ -83,6 +86,8 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
rootPath = null,
showParentLevel = true,
limit = 10,
+ showMore = false,
+ showMoreLimit = 20,
sortBy = ['name:asc'],
transformItems = items => items,
} = widgetParams;
@@ -91,13 +96,37 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
throw new Error(usage);
}
+ if (showMore === true && showMoreLimit <= limit) {
+ throw new Error('`showMoreLimit` must be greater than `limit`.');
+ }
+
// we need to provide a hierarchicalFacet name for the search state
// so that we can always map $hierarchicalFacetName => real attributes
// we use the first attribute name
const [hierarchicalFacetName] = attributes;
return {
- getConfiguration: currentConfiguration => {
+ isShowingMore: false,
+
+ // Provide the same function to the `renderFn` so that way the user
+ // has to only bind it once when `isFirstRendering` for instance
+ toggleShowMore() {},
+ cachedToggleShowMore() {
+ this.toggleShowMore();
+ },
+
+ createToggleShowMore(renderOptions) {
+ return () => {
+ this.isShowingMore = !this.isShowingMore;
+ this.render(renderOptions);
+ };
+ },
+
+ getLimit() {
+ return this.isShowingMore ? showMoreLimit : limit;
+ },
+
+ getConfiguration(currentConfiguration) {
if (currentConfiguration.hierarchicalFacets) {
const isFacetSet = find(
currentConfiguration.hierarchicalFacets,
@@ -111,13 +140,13 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
)
) {
warn(
- 'using Breadcrumb & HierarchicalMenu on the same facet with different options'
+ 'Using Breadcrumb and HierarchicalMenu on the same facet with different options overrides the configuration of the HierarchicalMenu.'
);
return {};
}
}
- return {
+ const widgetConfiguration = {
hierarchicalFacets: [
{
name: hierarchicalFacetName,
@@ -127,14 +156,21 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
showParentLevel,
},
],
- maxValuesPerFacet:
- currentConfiguration.maxValuesPerFacet !== undefined
- ? Math.max(currentConfiguration.maxValuesPerFacet, limit)
- : limit,
};
+
+ const currentMaxValuesPerFacet =
+ currentConfiguration.maxValuesPerFacet || 0;
+
+ widgetConfiguration.maxValuesPerFacet = Math.max(
+ currentMaxValuesPerFacet,
+ showMore ? showMoreLimit : limit
+ );
+
+ return widgetConfiguration;
},
init({ helper, createURL, instantSearchInstance }) {
+ this.cachedToggleShowMore = this.cachedToggleShowMore.bind(this);
this._refine = function(facetValue) {
helper.toggleRefinement(hierarchicalFacetName, facetValue).search();
};
@@ -148,11 +184,14 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
renderFn(
{
- createURL: _createURL,
items: [],
+ createURL: _createURL,
refine: this._refine,
instantSearchInstance,
widgetParams,
+ isShowingMore: false,
+ toggleShowMore: this.cachedToggleShowMore,
+ canToggleShowMore: false,
},
true
);
@@ -160,7 +199,7 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
_prepareFacetValues(facetValues, state) {
return facetValues
- .slice(0, limit)
+ .slice(0, this.getLimit())
.map(({ name: label, path: value, ...subValue }) => {
if (Array.isArray(subValue.data)) {
subValue.data = this._prepareFacetValues(subValue.data, state);
@@ -169,13 +208,19 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
});
},
- render({ results, state, createURL, instantSearchInstance }) {
+ render(renderOptions) {
+ const {
+ results,
+ state,
+ createURL,
+ instantSearchInstance,
+ } = renderOptions;
+
+ const facetValues =
+ results.getFacetValues(hierarchicalFacetName, { sortBy }).data || [];
const items = transformItems(
- this._prepareFacetValues(
- results.getFacetValues(hierarchicalFacetName, { sortBy }).data ||
- [],
- state
- )
+ this._prepareFacetValues(facetValues),
+ state
);
// Bind createURL to this specific attribute
@@ -185,13 +230,34 @@ export default function connectHierarchicalMenu(renderFn, unmountFn) {
);
}
+ const maxValuesPerFacetConfig = state.getQueryParameter(
+ 'maxValuesPerFacet'
+ );
+ const currentLimit = this.getLimit();
+ // If the limit is the max number of facet retrieved it is impossible to know
+ // if the facets are exhaustive. The only moment we are sure it is exhaustive
+ // is when it is strictly under the number requested unless we know that another
+ // widget has requested more values (maxValuesPerFacet > getLimit()).
+ // Because this is used for making the search of facets unable or not, it is important
+ // to be conservative here.
+ const hasExhaustiveItems =
+ maxValuesPerFacetConfig > currentLimit
+ ? facetValues.length <= currentLimit
+ : facetValues.length < currentLimit;
+
+ this.toggleShowMore = this.createToggleShowMore(renderOptions);
+
renderFn(
{
- createURL: _createURL,
items,
refine: this._refine,
+ createURL: _createURL,
instantSearchInstance,
widgetParams,
+ isShowingMore: this.isShowingMore,
+ toggleShowMore: this.cachedToggleShowMore,
+ canToggleShowMore:
+ showMore && (this.isShowingMore || !hasExhaustiveItems),
},
false
);
diff --git a/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js b/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
index b024b8f6b7..9bdc3eaa9a 100644
--- a/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
+++ b/src/connectors/hits-per-page/__tests__/connectHitsPerPage-test.js
@@ -205,6 +205,43 @@ describe('connectHitsPerPage', () => {
expect(helper.search).toHaveBeenCalledTimes(2);
});
+ it('provides a createURL function', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHitsPerPage(rendering);
+ const widget = makeWidget({
+ items: [
+ { value: 3, label: '3 items per page' },
+ { value: 10, label: '10 items per page' },
+ { value: 20, label: '20 items per page' },
+ ],
+ });
+ const helper = jsHelper({}, '', {
+ hitsPerPage: 20,
+ });
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: state => state,
+ });
+
+ const createURLAtInit = rendering.mock.calls[0][0].createURL;
+ expect(helper.getQueryParameter('hitsPerPage')).toEqual(20);
+ const URLStateAtInit = createURLAtInit(3);
+ expect(URLStateAtInit.hitsPerPage).toEqual(3);
+
+ widget.render({
+ results: new SearchResults(helper.state, [{}]),
+ state: helper.state,
+ createURL: state => state,
+ });
+
+ const createURLAtRender = rendering.mock.calls[1][0].createURL;
+ const URLStateAtRender = createURLAtRender(5);
+ expect(URLStateAtRender.hitsPerPage).toEqual(5);
+ });
+
it('provides the current hitsPerPage value', () => {
const rendering = jest.fn();
const makeWidget = connectHitsPerPage(rendering);
diff --git a/src/connectors/hits-per-page/connectHitsPerPage.js b/src/connectors/hits-per-page/connectHitsPerPage.js
index b6fbc8c4a7..7b2dd3711f 100644
--- a/src/connectors/hits-per-page/connectHitsPerPage.js
+++ b/src/connectors/hits-per-page/connectHitsPerPage.js
@@ -7,6 +7,7 @@ const usage = `Usage:
var customHitsPerPage = connectHitsPerPage(function render(params, isFirstRendering) {
// params = {
// items,
+ // createURL,
// refine,
// hasNoResults,
// instantSearchInstance,
@@ -43,6 +44,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} HitsPerPageRenderingOptions
* @property {HitsPerPageRenderingOptionsItem[]} items Array of objects defining the different values and labels.
+ * @property {function(item.value)} createURL Creates the URL for a single item name in the list.
* @property {function(number)} refine Sets the number of hits per page and trigger a search.
* @property {boolean} hasNoResults `true` if the last search contains no result.
* @property {Object} widgetParams Original `HitsPerPageWidgetOptions` forwarded to `renderFn`.
@@ -123,7 +125,7 @@ export default function connectHitsPerPage(renderFn, unmountFn) {
const defaultValues = items.filter(item => item.default);
if (defaultValues.length > 1) {
throw new Error(
- `[Error][hitsPerPageSelector] more than one default value is specified in \`items[]\`
+ `[Error][hitsPerPage] more than one default value is specified in \`items[]\`
The first one will be picked, you should probably set only one default value`
);
}
@@ -137,7 +139,7 @@ The first one will be picked, you should probably set only one default value`
: {};
},
- init({ helper, state, instantSearchInstance }) {
+ init({ helper, createURL, state, instantSearchInstance }) {
const isCurrentInOptions = some(
items,
item => Number(state.hitsPerPage) === Number(item.value)
@@ -145,20 +147,20 @@ The first one will be picked, you should probably set only one default value`
if (!isCurrentInOptions) {
if (state.hitsPerPage === undefined) {
- if (window.console) {
- warn(
- `[hitsPerPageSelector] hitsPerPage not defined.
- You should probably set the value \`hitsPerPage\`
- using the searchParameters attribute of the instantsearch constructor.`
- );
- }
- } else if (window.console) {
warn(
- `[hitsPerPageSelector] No item in \`items\`
- with \`value: hitsPerPage\` (hitsPerPage: ${state.hitsPerPage})`
+ `\`hitsPerPage\` is not defined.
+The option \`hitsPerPage\` needs to be set using the \`configure\` widget.
+
+Learn more: https://community.algolia.com/instantsearch.js/v2/widgets/configure.html`
);
}
+ warn(
+ `No items in HitsPerPage \`items\` with \`value: hitsPerPage\` (hitsPerPage: ${
+ state.hitsPerPage
+ })`
+ );
+
items = [{ value: '', label: '' }, ...items];
}
@@ -167,10 +169,19 @@ The first one will be picked, you should probably set only one default value`
? helper.setQueryParameter('hitsPerPage', undefined).search()
: helper.setQueryParameter('hitsPerPage', value).search();
+ this.createURL = helperState => value =>
+ createURL(
+ helperState.setQueryParameter(
+ 'hitsPerPage',
+ !value && value !== 0 ? undefined : value
+ )
+ );
+
renderFn(
{
items: transformItems(this._normalizeItems(state)),
refine: this.setHitsPerPage,
+ createURL: this.createURL(helper.state),
hasNoResults: true,
widgetParams,
instantSearchInstance,
@@ -186,6 +197,7 @@ The first one will be picked, you should probably set only one default value`
{
items: transformItems(this._normalizeItems(state)),
refine: this.setHitsPerPage,
+ createURL: this.createURL(state),
hasNoResults,
widgetParams,
instantSearchInstance,
diff --git a/src/connectors/hits/__tests__/connectHits-test.js b/src/connectors/hits/__tests__/connectHits-test.js
index 23f9f588cb..50e7123ff0 100644
--- a/src/connectors/hits/__tests__/connectHits-test.js
+++ b/src/connectors/hits/__tests__/connectHits-test.js
@@ -1,4 +1,5 @@
import jsHelper from 'algoliasearch-helper';
+import { TAG_PLACEHOLDER } from '../../../lib/escape-highlight.js';
const SearchResults = jsHelper.SearchResults;
import connectHits from '../connectHits.js';
@@ -9,11 +10,11 @@ describe('connectHits', () => {
// flag set accordingly
const rendering = jest.fn();
const makeWidget = connectHits(rendering);
- const widget = makeWidget({ escapeHits: true });
+ const widget = makeWidget({ escapeHTML: true });
expect(widget.getConfiguration()).toEqual({
- highlightPreTag: '__ais-highlight__',
- highlightPostTag: '__/ais-highlight__',
+ highlightPreTag: TAG_PLACEHOLDER.highlightPreTag,
+ highlightPostTag: TAG_PLACEHOLDER.highlightPostTag,
});
// test if widget is not rendered yet at this point
@@ -32,7 +33,7 @@ describe('connectHits', () => {
expect(rendering).toHaveBeenCalledTimes(1);
// test that rendering has been called during init with isFirstRendering = true
expect(rendering).toHaveBeenLastCalledWith(
- expect.objectContaining({ widgetParams: { escapeHits: true } }),
+ expect.objectContaining({ widgetParams: { escapeHTML: true } }),
true
);
@@ -46,11 +47,22 @@ describe('connectHits', () => {
expect(rendering).toHaveBeenCalledTimes(2);
// test that rendering has been called during init with isFirstRendering = false
expect(rendering).toHaveBeenLastCalledWith(
- expect.objectContaining({ widgetParams: { escapeHits: true } }),
+ expect.objectContaining({ widgetParams: { escapeHTML: true } }),
false
);
});
+ it('sets the default configuration', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHits(rendering);
+ const widget = makeWidget();
+
+ expect(widget.getConfiguration()).toEqual({
+ highlightPreTag: TAG_PLACEHOLDER.highlightPreTag,
+ highlightPostTag: TAG_PLACEHOLDER.highlightPostTag,
+ });
+ });
+
it('Provides the hits and the whole results', () => {
const rendering = jest.fn();
const makeWidget = connectHits(rendering);
@@ -75,6 +87,7 @@ describe('connectHits', () => {
);
const hits = [{ fake: 'data' }, { sample: 'infos' }];
+ hits.__escaped = true;
const results = new SearchResults(helper.state, [
{ hits: [].concat(hits) },
@@ -98,7 +111,7 @@ describe('connectHits', () => {
it('escape highlight properties if requested', () => {
const rendering = jest.fn();
const makeWidget = connectHits(rendering);
- const widget = makeWidget({ escapeHits: true });
+ const widget = makeWidget({ escapeHTML: true });
const helper = jsHelper({}, '', {});
helper.search = jest.fn();
@@ -122,7 +135,9 @@ describe('connectHits', () => {
{
_highlightResult: {
foobar: {
- value: '',
+ value: ``,
},
},
},
@@ -140,7 +155,7 @@ describe('connectHits', () => {
{
_highlightResult: {
foobar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
},
@@ -199,4 +214,87 @@ describe('connectHits', () => {
expect.anything()
);
});
+
+ it('transform items after escaping', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectHits(rendering);
+ const widget = makeWidget({
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ _highlightResult: {
+ name: {
+ value: item._highlightResult.name.value.toUpperCase(),
+ },
+ },
+ })),
+ escapeHTML: true,
+ });
+
+ const helper = jsHelper({}, '', {});
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const hits = [
+ {
+ name: 'hello',
+ _highlightResult: {
+ name: {
+ value: `he${TAG_PLACEHOLDER.highlightPreTag}llo${
+ TAG_PLACEHOLDER.highlightPostTag
+ }`,
+ },
+ },
+ },
+ {
+ name: 'halloween',
+ _highlightResult: {
+ name: {
+ value: `ha${TAG_PLACEHOLDER.highlightPreTag}llo${
+ TAG_PLACEHOLDER.highlightPostTag
+ }ween`,
+ },
+ },
+ },
+ ];
+
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ hits: [
+ {
+ name: 'hello',
+ _highlightResult: {
+ name: {
+ value: 'HELLO ',
+ },
+ },
+ },
+ {
+ name: 'halloween',
+ _highlightResult: {
+ name: {
+ value: 'HALLO WEEN',
+ },
+ },
+ },
+ ],
+ results,
+ }),
+ expect.anything()
+ );
+ });
});
diff --git a/src/connectors/hits/connectHits.js b/src/connectors/hits/connectHits.js
index f29df82d89..c981d179e2 100644
--- a/src/connectors/hits/connectHits.js
+++ b/src/connectors/hits/connectHits.js
@@ -1,4 +1,4 @@
-import escapeHits, { tagConfig } from '../../lib/escape-highlight.js';
+import escapeHits, { TAG_PLACEHOLDER } from '../../lib/escape-highlight.js';
import { checkRendering } from '../../lib/utils.js';
const usage = `Usage:
@@ -12,7 +12,7 @@ var customHits = connectHits(function render(params, isFirstRendering) {
});
search.addWidget(
customHits({
- [ escapeHits = false ],
+ [ escapeHTML = true ],
[ transformItems ]
})
);
@@ -28,7 +28,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomHitsWidgetOptions
- * @property {boolean} [escapeHits = false] If true, escape HTML tags from `hits[i]._highlightResult`.
+ * @property {boolean} [escapeHTML = true] Whether to escape HTML tags from `hits[i]._highlightResult`.
* @property {function(Object[]):Object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -62,11 +62,11 @@ export default function connectHits(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { transformItems = items => items } = widgetParams;
+ const { escapeHTML = true, transformItems = items => items } = widgetParams;
return {
getConfiguration() {
- return widgetParams.escapeHits ? tagConfig : undefined;
+ return escapeHTML ? TAG_PLACEHOLDER : undefined;
},
init({ instantSearchInstance }) {
@@ -82,16 +82,12 @@ export default function connectHits(renderFn, unmountFn) {
},
render({ results, instantSearchInstance }) {
- results.hits = transformItems(results.hits);
-
- if (
- widgetParams.escapeHits &&
- results.hits &&
- results.hits.length > 0
- ) {
+ if (escapeHTML && results.hits && results.hits.length > 0) {
results.hits = escapeHits(results.hits);
}
+ results.hits = transformItems(results.hits);
+
renderFn(
{
hits: results.hits,
diff --git a/src/connectors/index.js b/src/connectors/index.js
index f76ad9019f..e0544acffe 100644
--- a/src/connectors/index.js
+++ b/src/connectors/index.js
@@ -1,7 +1,9 @@
-export { default as connectClearAll } from './clear-all/connectClearAll.js';
export {
- default as connectCurrentRefinedValues,
-} from './current-refined-values/connectCurrentRefinedValues.js';
+ default as connectClearRefinements,
+} from './clear-refinements/connectClearRefinements';
+export {
+ default as connectCurrentRefinements,
+} from './current-refinements/connectCurrentRefinements.js';
export {
default as connectHierarchicalMenu,
} from './hierarchical-menu/connectHierarchicalMenu.js';
@@ -14,37 +16,27 @@ export {
} from './infinite-hits/connectInfiniteHits.js';
export { default as connectMenu } from './menu/connectMenu.js';
export {
- default as connectNumericRefinementList,
-} from './numeric-refinement-list/connectNumericRefinementList.js';
-export {
- default as connectNumericSelector,
-} from './numeric-selector/connectNumericSelector.js';
+ default as connectNumericMenu,
+} from './numeric-menu/connectNumericMenu';
export {
default as connectPagination,
} from './pagination/connectPagination.js';
-export {
- default as connectPriceRanges,
-} from './price-ranges/connectPriceRanges.js';
-export {
- default as connectRangeSlider,
-} from './range-slider/connectRangeSlider.js';
export { default as connectRange } from './range/connectRange.js';
export {
default as connectRefinementList,
} from './refinement-list/connectRefinementList.js';
export { default as connectSearchBox } from './search-box/connectSearchBox.js';
-export {
- default as connectSortBySelector,
-} from './sort-by-selector/connectSortBySelector.js';
-export {
- default as connectStarRating,
-} from './star-rating/connectStarRating.js';
+export { default as connectSortBy } from './sort-by/connectSortBy.js';
+export { default as connectRatingMenu } from './rating-menu/connectRatingMenu';
export { default as connectStats } from './stats/connectStats.js';
-export { default as connectToggle } from './toggle/connectToggle.js';
+export {
+ default as connectToggleRefinement,
+} from './toggleRefinement/connectToggleRefinement.js';
export {
default as connectBreadcrumb,
} from './breadcrumb/connectBreadcrumb.js';
export { default as connectGeoSearch } from './geo-search/connectGeoSearch.js';
+export { default as connectPoweredBy } from './powered-by/connectPoweredBy.js';
export { default as connectConfigure } from './configure/connectConfigure.js';
export {
default as connectAutocomplete,
diff --git a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
index 3c88cd8512..5c28bb320c 100644
--- a/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
+++ b/src/connectors/infinite-hits/__tests__/connectInfiniteHits-test.js
@@ -1,4 +1,5 @@
import jsHelper from 'algoliasearch-helper';
+import { TAG_PLACEHOLDER } from '../../../lib/escape-highlight.js';
const SearchResults = jsHelper.SearchResults;
import connectInfiniteHits from '../connectInfiniteHits.js';
@@ -10,12 +11,12 @@ describe('connectInfiniteHits', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
const widget = makeWidget({
- escapeHits: true,
+ escapeHTML: true,
});
expect(widget.getConfiguration()).toEqual({
- highlightPostTag: '__/ais-highlight__',
- highlightPreTag: '__ais-highlight__',
+ highlightPreTag: TAG_PLACEHOLDER.highlightPreTag,
+ highlightPostTag: TAG_PLACEHOLDER.highlightPostTag,
});
// test if widget is not rendered yet at this point
@@ -42,7 +43,7 @@ describe('connectInfiniteHits', () => {
isLastPage: true,
instantSearchInstance: undefined,
widgetParams: {
- escapeHits: true,
+ escapeHTML: true,
},
}),
true
@@ -68,13 +69,24 @@ describe('connectInfiniteHits', () => {
isLastPage: false,
instantSearchInstance: undefined,
widgetParams: {
- escapeHits: true,
+ escapeHTML: true,
},
}),
false
);
});
+ it('sets the default configuration', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectInfiniteHits(rendering);
+ const widget = makeWidget();
+
+ expect(widget.getConfiguration()).toEqual({
+ highlightPreTag: TAG_PLACEHOLDER.highlightPreTag,
+ highlightPostTag: TAG_PLACEHOLDER.highlightPostTag,
+ });
+ });
+
it('Provides the hits and the whole results', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
@@ -153,7 +165,7 @@ describe('connectInfiniteHits', () => {
it('escape highlight properties if requested', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
- const widget = makeWidget({ escapeHits: true });
+ const widget = makeWidget({ escapeHTML: true });
const helper = jsHelper({}, '', {});
helper.search = jest.fn();
@@ -173,7 +185,9 @@ describe('connectInfiniteHits', () => {
{
_highlightResult: {
foobar: {
- value: '',
+ value: ``,
},
},
},
@@ -191,7 +205,7 @@ describe('connectInfiniteHits', () => {
{
_highlightResult: {
foobar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
},
@@ -254,6 +268,89 @@ describe('connectInfiniteHits', () => {
expect(secondRenderingOptions.results).toEqual(results);
});
+ it('transform items after escaping', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectInfiniteHits(rendering);
+ const widget = makeWidget({
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ _highlightResult: {
+ name: {
+ value: item._highlightResult.name.value.toUpperCase(),
+ },
+ },
+ })),
+ escapeHTML: true,
+ });
+
+ const helper = jsHelper({}, '', {});
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ });
+
+ const hits = [
+ {
+ name: 'hello',
+ _highlightResult: {
+ name: {
+ value: `he${TAG_PLACEHOLDER.highlightPreTag}llo${
+ TAG_PLACEHOLDER.highlightPostTag
+ }`,
+ },
+ },
+ },
+ {
+ name: 'halloween',
+ _highlightResult: {
+ name: {
+ value: `ha${TAG_PLACEHOLDER.highlightPreTag}llo${
+ TAG_PLACEHOLDER.highlightPostTag
+ }ween`,
+ },
+ },
+ },
+ ];
+
+ const results = new SearchResults(helper.state, [{ hits }]);
+ widget.render({
+ results,
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ expect(rendering).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ hits: [
+ {
+ name: 'hello',
+ _highlightResult: {
+ name: {
+ value: 'HELLO ',
+ },
+ },
+ },
+ {
+ name: 'halloween',
+ _highlightResult: {
+ name: {
+ value: 'HALLO WEEN',
+ },
+ },
+ },
+ ],
+ results,
+ }),
+ expect.anything()
+ );
+ });
+
it('does not render the same page twice', () => {
const rendering = jest.fn();
const makeWidget = connectInfiniteHits(rendering);
diff --git a/src/connectors/infinite-hits/connectInfiniteHits.js b/src/connectors/infinite-hits/connectInfiniteHits.js
index 1bf7a08e3e..867afb8be6 100644
--- a/src/connectors/infinite-hits/connectInfiniteHits.js
+++ b/src/connectors/infinite-hits/connectInfiniteHits.js
@@ -1,4 +1,4 @@
-import escapeHits, { tagConfig } from '../../lib/escape-highlight.js';
+import escapeHits, { TAG_PLACEHOLDER } from '../../lib/escape-highlight.js';
import { checkRendering } from '../../lib/utils.js';
const usage = `Usage:
@@ -14,8 +14,8 @@ var customInfiniteHits = connectInfiniteHits(function render(params, isFirstRend
});
search.addWidget(
customInfiniteHits({
- [ escapeHits: true ],
- [ transformItems ]
+ [ escapeHTML = true ],
+ [ transformItems ],
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectInfiniteHits.html
@@ -32,7 +32,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomInfiniteHitsWidgetOptions
- * @property {boolean} [escapeHits = false] If true, escape HTML tags from `hits[i]._highlightResult`.
+ * @property {boolean} [escapeHTML = true] Whether to escape HTML tags from `hits[i]._highlightResult`.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -80,7 +80,7 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { transformItems = items => items } = widgetParams;
+ const { escapeHTML = true, transformItems = items => items } = widgetParams;
let hitsCache = [];
let lastReceivedPage = -1;
@@ -88,7 +88,7 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
return {
getConfiguration() {
- return widgetParams.escapeHits ? tagConfig : undefined;
+ return escapeHTML ? TAG_PLACEHOLDER : undefined;
},
init({ instantSearchInstance, helper }) {
@@ -113,16 +113,12 @@ export default function connectInfiniteHits(renderFn, unmountFn) {
lastReceivedPage = -1;
}
- results.hits = transformItems(results.hits);
-
- if (
- widgetParams.escapeHits &&
- results.hits &&
- results.hits.length > 0
- ) {
+ if (escapeHTML && results.hits && results.hits.length > 0) {
results.hits = escapeHits(results.hits);
}
+ results.hits = transformItems(results.hits);
+
if (lastReceivedPage < state.page) {
hitsCache = [...hitsCache, ...results.hits];
lastReceivedPage = state.page;
diff --git a/src/connectors/menu/__tests__/connectMenu-test.js b/src/connectors/menu/__tests__/connectMenu-test.js
index 14a6d55224..94af80e515 100644
--- a/src/connectors/menu/__tests__/connectMenu-test.js
+++ b/src/connectors/menu/__tests__/connectMenu-test.js
@@ -26,9 +26,9 @@ describe('connectMenu', () => {
});
describe('options configuring the helper', () => {
- it('`attributeName`', () => {
+ it('`attribute`', () => {
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
});
expect(widget.getConfiguration({})).toEqual({
@@ -44,7 +44,7 @@ describe('connectMenu', () => {
it('`limit`', () => {
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 20,
});
@@ -64,7 +64,7 @@ describe('connectMenu', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
});
@@ -99,7 +99,7 @@ describe('connectMenu', () => {
expect.objectContaining({
canRefine: false,
widgetParams: {
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
},
}),
@@ -119,7 +119,7 @@ describe('connectMenu', () => {
expect.objectContaining({
canRefine: false,
widgetParams: {
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
},
}),
@@ -129,7 +129,7 @@ describe('connectMenu', () => {
it('Provide a function to clear the refinements at each step', () => {
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
@@ -168,7 +168,7 @@ describe('connectMenu', () => {
it('provides the correct facet values', () => {
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
@@ -240,7 +240,7 @@ describe('connectMenu', () => {
it('provides the correct transformed facet values', () => {
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
transformItems: items =>
items.map(item => ({
...item,
@@ -300,21 +300,30 @@ describe('connectMenu', () => {
});
describe('showMore', () => {
- it('should throw when `showMoreLimit` is lower than `limit`', () => {
- expect(() =>
- connectMenu(() => {})({
- attributeName: 'myFacet',
- limit: 10,
- showMoreLimit: 1,
- })
- ).toThrow();
+ it('should set `maxValuesPerFacet` by default', () => {
+ const widget = makeWidget({
+ attribute: 'myFacet',
+ limit: 10,
+ showMore: true,
+ });
+
+ expect(widget.getConfiguration({})).toEqual({
+ hierarchicalFacets: [
+ {
+ name: 'myFacet',
+ attributes: ['myFacet'],
+ },
+ ],
+ maxValuesPerFacet: 20,
+ });
});
it('should provide `showMoreLimit` as `maxValuesPerFacet`', () => {
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 10,
- showMoreLimit: 20,
+ showMore: true,
+ showMoreLimit: 30,
});
expect(widget.getConfiguration({})).toEqual({
@@ -324,15 +333,16 @@ describe('connectMenu', () => {
attributes: ['myFacet'],
},
],
- maxValuesPerFacet: 20,
+ maxValuesPerFacet: 30,
});
});
it('should initialize with `isShowingMore === false`', () => {
// Given
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 10,
+ showMore: true,
showMoreLimit: 20,
});
@@ -359,8 +369,9 @@ describe('connectMenu', () => {
it('should toggle `isShowingMore` when `toggleShowMore` is called', () => {
// Given
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 1,
+ showMore: true,
showMoreLimit: 2,
});
@@ -423,8 +434,9 @@ describe('connectMenu', () => {
it('should set canToggleShowMore to false when there are not enough items', () => {
// Given
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 1,
+ showMore: true,
showMoreLimit: 2,
});
@@ -477,7 +489,7 @@ describe('connectMenu', () => {
const rendering2 = jest.fn();
const makeWidget2 = connectMenu(rendering2);
const widget = makeWidget2({
- attributeName: 'category',
+ attribute: 'category',
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
diff --git a/src/connectors/menu/connectMenu.js b/src/connectors/menu/connectMenu.js
index d5deda0e39..dfe221428d 100644
--- a/src/connectors/menu/connectMenu.js
+++ b/src/connectors/menu/connectMenu.js
@@ -15,10 +15,11 @@ var customMenu = connectMenu(function render(params, isFirstRendering) {
});
search.addWidget(
customMenu({
- attributeName,
- [ limit ],
- [ showMoreLimit ]
- [ sortBy = ['name:asc'] ]
+ attribute,
+ [ limit = 10 ],
+ [ showMore = false ],
+ [ showMoreLimit = 20 ],
+ [ sortBy = ['isRefined', 'name:asc'] ],
[ transformItems ]
})
);
@@ -30,15 +31,16 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* @property {string} value The value of the menu item.
* @property {string} label Human-readable value of the menu item.
* @property {number} count Number of results matched after refinement is applied.
- * @property {isRefined} boolean Indicates if the refinement is applied.
+ * @property {boolean} isRefined Indicates if the refinement is applied.
*/
/**
* @typedef {Object} CustomMenuWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
+ * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping").
* @property {number} [limit = 10] How many facets values to retrieve.
- * @property {number} [showMoreLimit = undefined] How many facets values to retrieve when `toggleShowMore` is called, this value is meant to be greater than `limit` option.
- * @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
+ * @property {boolean} [showMore = false] Whether to display a button that expands the number of items.
+ * @property {number} [showMoreLimit = 20] How many facets values to retrieve when `toggleShowMore` is called, this value is meant to be greater than `limit` option.
+ * @property {string[]|function} [sortBy = ['isRefined', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
@@ -64,7 +66,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* function to select an item. While selecting a new element, the `refine` will also unselect the
* one that is currently selected.
*
- * **Requirement:** the attribute passed as `attributeName` must be present in "attributes for faceting" on the Algolia dashboard or configured as attributesForFaceting via a set settings call to the Algolia API.
+ * **Requirement:** the attribute passed as `attribute` must be present in "attributes for faceting" on the Algolia dashboard or configured as attributesForFaceting via a set settings call to the Algolia API.
* @type {Connector}
* @param {function(MenuRenderingOptions, boolean)} renderFn Rendering function for the custom **Menu** widget. widget.
* @param {function} unmountFn Unmount function called when the widget is disposed.
@@ -101,7 +103,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* search.addWidget(
* customMenu({
* containerNode: $('#custom-menu-container'),
- * attributeName: 'categories',
+ * attribute: 'categories',
* limit: 10,
* })
* );
@@ -111,17 +113,22 @@ export default function connectMenu(renderFn, unmountFn) {
return (widgetParams = {}) => {
const {
- attributeName,
+ attribute,
limit = 10,
- sortBy = ['name:asc'],
- showMoreLimit,
+ showMore = false,
+ showMoreLimit = 20,
+ sortBy = ['isRefined', 'name:asc'],
transformItems = items => items,
} = widgetParams;
- if (!attributeName || (!isNaN(showMoreLimit) && showMoreLimit < limit)) {
+ if (!attribute) {
throw new Error(usage);
}
+ if (showMore === true && showMoreLimit <= limit) {
+ throw new Error('`showMoreLimit` should be greater than `limit`.');
+ }
+
return {
isShowingMore: false,
@@ -146,13 +153,10 @@ export default function connectMenu(renderFn, unmountFn) {
refine(helper) {
return facetValue => {
const [refinedItem] = helper.getHierarchicalFacetBreadcrumb(
- attributeName
+ attribute
);
helper
- .toggleRefinement(
- attributeName,
- facetValue ? facetValue : refinedItem
- )
+ .toggleRefinement(attribute, facetValue ? facetValue : refinedItem)
.search();
};
},
@@ -161,8 +165,8 @@ export default function connectMenu(renderFn, unmountFn) {
const widgetConfiguration = {
hierarchicalFacets: [
{
- name: attributeName,
- attributes: [attributeName],
+ name: attribute,
+ attributes: [attribute],
},
],
};
@@ -170,7 +174,7 @@ export default function connectMenu(renderFn, unmountFn) {
const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
widgetConfiguration.maxValuesPerFacet = Math.max(
currentMaxValuesPerFacet,
- showMoreLimit || limit
+ showMore ? showMoreLimit : limit
);
return widgetConfiguration;
@@ -180,7 +184,7 @@ export default function connectMenu(renderFn, unmountFn) {
this.cachedToggleShowMore = this.cachedToggleShowMore.bind(this);
this._createURL = facetValue =>
- createURL(helper.state.toggleRefinement(attributeName, facetValue));
+ createURL(helper.state.toggleRefinement(attribute, facetValue));
this._refine = this.refine(helper);
@@ -202,7 +206,7 @@ export default function connectMenu(renderFn, unmountFn) {
render({ results, instantSearchInstance }) {
const facetItems =
- results.getFacetValues(attributeName, { sortBy }).data || [];
+ results.getFacetValues(attribute, { sortBy }).data || [];
const items = transformItems(
facetItems
.slice(0, this.getLimit())
@@ -229,7 +233,8 @@ export default function connectMenu(renderFn, unmountFn) {
isShowingMore: this.isShowingMore,
toggleShowMore: this.cachedToggleShowMore,
canToggleShowMore:
- this.isShowingMore || facetItems.length > this.getLimit(),
+ showMore &&
+ (this.isShowingMore || facetItems.length > this.getLimit()),
},
false
);
@@ -240,11 +245,11 @@ export default function connectMenu(renderFn, unmountFn) {
let nextState = state;
- if (state.isHierarchicalFacetRefined(attributeName)) {
- nextState = state.removeHierarchicalFacetRefinement(attributeName);
+ if (state.isHierarchicalFacetRefined(attribute)) {
+ nextState = state.removeHierarchicalFacetRefinement(attribute);
}
- nextState = nextState.removeHierarchicalFacet(attributeName);
+ nextState = nextState.removeHierarchicalFacet(attribute);
if (
nextState.maxValuesPerFacet === limit ||
@@ -258,12 +263,12 @@ export default function connectMenu(renderFn, unmountFn) {
getWidgetState(uiState, { searchParameters }) {
const [refinedItem] = searchParameters.getHierarchicalFacetBreadcrumb(
- attributeName
+ attribute
);
if (
!refinedItem ||
- (uiState.menu && uiState.menu[attributeName] === refinedItem)
+ (uiState.menu && uiState.menu[attribute] === refinedItem)
) {
return uiState;
}
@@ -272,29 +277,29 @@ export default function connectMenu(renderFn, unmountFn) {
...uiState,
menu: {
...uiState.menu,
- [attributeName]: refinedItem,
+ [attribute]: refinedItem,
},
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
- if (uiState.menu && uiState.menu[attributeName]) {
- const uiStateRefinedItem = uiState.menu[attributeName];
+ if (uiState.menu && uiState.menu[attribute]) {
+ const uiStateRefinedItem = uiState.menu[attribute];
const isAlreadyRefined = searchParameters.isHierarchicalFacetRefined(
- attributeName,
+ attribute,
uiStateRefinedItem
);
if (isAlreadyRefined) return searchParameters;
return searchParameters.toggleRefinement(
- attributeName,
+ attribute,
uiStateRefinedItem
);
}
- if (searchParameters.isHierarchicalFacetRefined(attributeName)) {
+ if (searchParameters.isHierarchicalFacetRefined(attribute)) {
const [refinedItem] = searchParameters.getHierarchicalFacetBreadcrumb(
- attributeName
+ attribute
);
- return searchParameters.toggleRefinement(attributeName, refinedItem);
+ return searchParameters.toggleRefinement(attribute, refinedItem);
}
return searchParameters;
},
diff --git a/src/connectors/numeric-menu/__tests__/__snapshots__/connectNumericMenu-test.js.snap b/src/connectors/numeric-menu/__tests__/__snapshots__/connectNumericMenu-test.js.snap
new file mode 100644
index 0000000000..037317a676
--- /dev/null
+++ b/src/connectors/numeric-menu/__tests__/__snapshots__/connectNumericMenu-test.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`connectNumericMenu routing getWidgetState should add an entry equal to the refinement (equal) 1`] = `
+Object {
+ "numericMenu": Object {
+ "numerics": "20",
+ },
+}
+`;
+
+exports[`connectNumericMenu routing getWidgetState should add an entry equal to the refinement (only max) 1`] = `
+Object {
+ "numericMenu": Object {
+ "numerics": ":20",
+ },
+}
+`;
+
+exports[`connectNumericMenu routing getWidgetState should add an entry equal to the refinement (only min) 1`] = `
+Object {
+ "numericMenu": Object {
+ "numerics": "10:",
+ },
+}
+`;
+
+exports[`connectNumericMenu routing getWidgetState should add an entry equal to the refinement (range) 1`] = `
+Object {
+ "numericMenu": Object {
+ "numerics": "10:20",
+ },
+}
+`;
+
+exports[`connectNumericMenu routing getWidgetState should not override other values in the same namespace 1`] = `
+Object {
+ "numericMenu": Object {
+ "numerics": "10:20",
+ "numerics-2": "27:36",
+ },
+}
+`;
diff --git a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.js
similarity index 85%
rename from src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js
rename to src/connectors/numeric-menu/__tests__/connectNumericMenu-test.js
index 69d0e10840..6d1b76e009 100644
--- a/src/connectors/numeric-refinement-list/__tests__/connectNumericRefinementList-test.js
+++ b/src/connectors/numeric-menu/__tests__/connectNumericMenu-test.js
@@ -3,28 +3,28 @@ import jsHelper, {
SearchParameters,
} from 'algoliasearch-helper';
-import connectNumericRefinementList from '../connectNumericRefinementList.js';
+import connectNumericMenu from '../connectNumericMenu.js';
const encodeValue = (start, end) =>
window.encodeURI(JSON.stringify({ start, end }));
-const mapOptionsToItems = ({ start, end, name: label }) => ({
+const mapOptionsToItems = ({ start, end, label }) => ({
label,
value: encodeValue(start, end),
isRefined: false,
});
-describe('connectNumericRefinementList', () => {
+describe('connectNumericMenu', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
],
});
@@ -49,11 +49,11 @@ describe('connectNumericRefinementList', () => {
expect(rendering).toHaveBeenLastCalledWith(
expect.objectContaining({
widgetParams: {
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
],
},
}),
@@ -72,11 +72,11 @@ describe('connectNumericRefinementList', () => {
expect(rendering).toHaveBeenLastCalledWith(
expect.objectContaining({
widgetParams: {
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
],
},
}),
@@ -86,10 +86,10 @@ describe('connectNumericRefinementList', () => {
it('Renders during init and render with transformed items', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [{ name: 'below 10', end: 10 }],
+ attribute: 'numerics',
+ items: [{ label: 'below 10', end: 10 }],
transformItems: items =>
items.map(item => ({
...item,
@@ -131,15 +131,15 @@ describe('connectNumericRefinementList', () => {
it('Provide a function to update the refinements at each step', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- { name: '42', start: 42, end: 42 },
- { name: 'void' },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
+ { label: '42', start: 42, end: 42 },
+ { label: 'void' },
],
});
@@ -212,13 +212,13 @@ describe('connectNumericRefinementList', () => {
it('provides the correct facet values', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
],
});
@@ -272,17 +272,17 @@ describe('connectNumericRefinementList', () => {
it('provides isRefined for the currently selected value', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const listOptions = [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- { name: '42', start: 42, end: 42 },
- { name: 'void' },
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
+ { label: '42', start: 42, end: 42 },
+ { label: 'void' },
];
const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
+ attribute: 'numerics',
+ items: listOptions,
});
const helper = jsHelper({});
@@ -325,17 +325,17 @@ describe('connectNumericRefinementList', () => {
it('when the state is cleared, the "no value" value should be refined', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const listOptions = [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- { name: '42', start: 42, end: 42 },
- { name: 'void' },
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
+ { label: '42', start: 42, end: 42 },
+ { label: 'void' },
];
const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
+ attribute: 'numerics',
+ items: listOptions,
});
const helper = jsHelper({});
@@ -389,17 +389,17 @@ describe('connectNumericRefinementList', () => {
it('should set `isRefined: true` after calling `refine(item)`', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const listOptions = [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- { name: '42', start: 42, end: 42 },
- { name: 'void' },
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
+ { label: '42', start: 42, end: 42 },
+ { label: 'void' },
];
const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
+ attribute: 'numerics',
+ items: listOptions,
});
const helper = jsHelper({});
@@ -433,16 +433,16 @@ describe('connectNumericRefinementList', () => {
it('should reset page on refine()', () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
- { name: '42', start: 42, end: 42 },
- { name: 'void' },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
+ { label: '42', start: 42, end: 42 },
+ { label: 'void' },
],
});
@@ -470,13 +470,13 @@ describe('connectNumericRefinementList', () => {
describe('routing', () => {
const getInitializedWidget = () => {
const rendering = jest.fn();
- const makeWidget = connectNumericRefinementList(rendering);
+ const makeWidget = connectNumericMenu(rendering);
const widget = makeWidget({
- attributeName: 'numerics',
- options: [
- { name: 'below 10', end: 10 },
- { name: '10 - 20', start: 10, end: 20 },
- { name: 'more than 20', start: 20 },
+ attribute: 'numerics',
+ items: [
+ { label: 'below 10', end: 10 },
+ { label: '10 - 20', start: 10, end: 20 },
+ { label: 'more than 20', start: 20 },
],
});
@@ -559,7 +559,7 @@ describe('connectNumericRefinementList', () => {
test('should not override other values in the same namespace', () => {
const [widget, helper] = getInitializedWidget();
const uiStateBefore = {
- numericRefinementList: {
+ numericMenu: {
'numerics-2': '27:36',
},
};
@@ -576,7 +576,7 @@ describe('connectNumericRefinementList', () => {
test('should give back the object unmodified if refinements are already set', () => {
const [widget, helper] = getInitializedWidget();
const uiStateBefore = {
- numericRefinementList: {
+ numericMenu: {
numerics: '10:20',
},
};
@@ -610,7 +610,7 @@ describe('connectNumericRefinementList', () => {
const [widget, helper] = getInitializedWidget();
// The URL state has some parameters
const uiState = {
- numericRefinementList: {
+ numericMenu: {
numerics: '10:',
},
};
@@ -635,7 +635,7 @@ describe('connectNumericRefinementList', () => {
const [widget, helper] = getInitializedWidget();
// The URL state has some parameters
const uiState = {
- numericRefinementList: {
+ numericMenu: {
numerics: ':20',
},
};
@@ -661,7 +661,7 @@ describe('connectNumericRefinementList', () => {
const [widget, helper] = getInitializedWidget();
// The URL state has some parameters
const uiState = {
- numericRefinementList: {
+ numericMenu: {
numerics: '10:20',
},
};
@@ -687,7 +687,7 @@ describe('connectNumericRefinementList', () => {
const [widget, helper] = getInitializedWidget();
// The URL state has some parameters
const uiState = {
- numericRefinementList: {
+ numericMenu: {
numerics: '10',
},
};
diff --git a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js b/src/connectors/numeric-menu/connectNumericMenu.js
similarity index 65%
rename from src/connectors/numeric-refinement-list/connectNumericRefinementList.js
rename to src/connectors/numeric-menu/connectNumericMenu.js
index 7a3a4b6e4d..523e580899 100644
--- a/src/connectors/numeric-refinement-list/connectNumericRefinementList.js
+++ b/src/connectors/numeric-menu/connectNumericMenu.js
@@ -3,7 +3,7 @@ import _isFinite from 'lodash/isFinite';
import { checkRendering } from '../../lib/utils.js';
const usage = `Usage:
-var customNumericRefinementList = connectNumericRefinementList(function renderFn(params, isFirstRendering) {
+var customNumericMenu = connectNumericMenu(function renderFn(params, isFirstRendering) {
// params = {
// createURL,
// items,
@@ -15,95 +15,95 @@ var customNumericRefinementList = connectNumericRefinementList(function renderFn
});
search.addWidget(
- customNumericRefinementList({
- attributeName,
- options,
+ customNumericMenu({
+ attribute,
+ items,
[ transformItems ],
})
);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectNumericRefinementList.html
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectNumericMenu.html
`;
/**
- * @typedef {Object} NumericRefinementListOption
+ * @typedef {Object} NumericMenuOption
* @property {string} name Name of the option.
* @property {number} start Lower bound of the option (>=).
* @property {number} end Higher bound of the option (<=).
*/
/**
- * @typedef {Object} NumericRefinementListItem
+ * @typedef {Object} NumericMenuItem
* @property {string} label Name of the option.
* @property {string} value URL encoded of the bounds object with the form `{start, end}`. This value can be used verbatim in the webpage and can be read by `refine` directly. If you want to inspect the value, you can do `JSON.parse(window.decodeURI(value))` to get the object.
* @property {boolean} isRefined True if the value is selected.
*/
/**
- * @typedef {Object} CustomNumericRefinementListWidgetOptions
- * @property {string} attributeName Name of the attribute for filtering.
- * @property {NumericRefinementListOption[]} options List of all the options.
+ * @typedef {Object} CustomNumericMenuWidgetOptions
+ * @property {string} attribute Name of the attribute for filtering.
+ * @property {NumericMenuOption[]} items List of all the items.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
- * @typedef {Object} NumericRefinementListRenderingOptions
+ * @typedef {Object} NumericMenuRenderingOptions
* @property {function(item.value): string} createURL Creates URLs for the next state, the string is the name of the selected option.
- * @property {NumericRefinementListItem[]} items The list of available choices.
+ * @property {NumericMenuItem[]} items The list of available choices.
* @property {boolean} hasNoResults `true` if the last search contains no result.
* @property {function(item.value)} refine Sets the selected value and trigger a new search.
- * @property {Object} widgetParams All original `CustomNumericRefinementListWidgetOptions` forwarded to the `renderFn`.
+ * @property {Object} widgetParams All original `CustomNumericMenuWidgetOptions` forwarded to the `renderFn`.
*/
/**
- * **NumericRefinementList** connector provides the logic to build a custom widget that will give the user the ability to choose a range on to refine the search results.
+ * **NumericMenu** connector provides the logic to build a custom widget that will give the user the ability to choose a range on to refine the search results.
*
* It provides a `refine(item)` function to refine on the selected range.
*
- * **Requirement:** the attribute passed as `attributeName` must be present in "attributes for faceting" on the Algolia dashboard or configured as attributesForFaceting via a set settings call to the Algolia API.
- * @function connectNumericRefinementList
+ * **Requirement:** the attribute passed as `attribute` must be present in "attributes for faceting" on the Algolia dashboard or configured as attributesForFaceting via a set settings call to the Algolia API.
+ * @function connectNumericMenu
* @type {Connector}
- * @param {function(NumericRefinementListRenderingOptions, boolean)} renderFn Rendering function for the custom **NumericRefinementList** widget.
+ * @param {function(NumericMenuRenderingOptions, boolean)} renderFn Rendering function for the custom **NumericMenu** widget.
* @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomNumericRefinementListWidgetOptions)} Re-usable widget factory for a custom **NumericRefinementList** widget.
+ * @return {function(CustomNumericMenuWidgetOptions)} Re-usable widget factory for a custom **NumericMenu** widget.
* @example
- * // custom `renderFn` to render the custom NumericRefinementList widget
- * function renderFn(NumericRefinementListRenderingOptions, isFirstRendering) {
+ * // custom `renderFn` to render the custom NumericMenu widget
+ * function renderFn(NumericMenuRenderingOptions, isFirstRendering) {
* if (isFirstRendering) {
- * NumericRefinementListRenderingOptions.widgetParams.containerNode.html('');
+ * NumericMenuRenderingOptions.widgetParams.containerNode.html('');
* }
*
- * NumericRefinementListRenderingOptions.widgetParams.containerNode
+ * NumericMenuRenderingOptions.widgetParams.containerNode
* .find('li[data-refine-value]')
* .each(function() { $(this).off('click'); });
*
- * var list = NumericRefinementListRenderingOptions.items.map(function(item) {
+ * var list = NumericMenuRenderingOptions.items.map(function(item) {
* return '' +
* ' ' +
* item.label + ' ';
* });
*
- * NumericRefinementListRenderingOptions.widgetParams.containerNode.find('ul').html(list);
- * NumericRefinementListRenderingOptions.widgetParams.containerNode
+ * NumericMenuRenderingOptions.widgetParams.containerNode.find('ul').html(list);
+ * NumericMenuRenderingOptions.widgetParams.containerNode
* .find('li[data-refine-value]')
* .each(function() {
* $(this).on('click', function(event) {
* event.preventDefault();
* event.stopPropagation();
- * NumericRefinementListRenderingOptions.refine($(this).data('refine-value'));
+ * NumericMenuRenderingOptions.refine($(this).data('refine-value'));
* });
* });
* }
*
- * // connect `renderFn` to NumericRefinementList logic
- * var customNumericRefinementList = instantsearch.connectors.connectNumericRefinementList(renderFn);
+ * // connect `renderFn` to NumericMenu logic
+ * var customNumericMenu = instantsearch.connectors.connectNumericMenu(renderFn);
*
* // mount widget on the page
* search.addWidget(
- * customNumericRefinementList({
- * containerNode: $('#custom-numeric-refinement-container'),
- * attributeName: 'price',
- * options: [
+ * customNumericMenu({
+ * containerNode: $('#custom-numeric-menu-container'),
+ * attribute: 'price',
+ * items: [
* {name: 'All'},
* {end: 4, name: 'less than 4'},
* {start: 4, end: 4, name: '4'},
@@ -113,17 +113,13 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* })
* );
*/
-export default function connectNumericRefinementList(renderFn, unmountFn) {
+export default function connectNumericMenu(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const {
- attributeName,
- options,
- transformItems = items => items,
- } = widgetParams;
+ const { attribute, items, transformItems = x => x } = widgetParams;
- if (!attributeName || !options) {
+ if (!attribute || !items) {
throw new Error(usage);
}
@@ -132,20 +128,20 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
this._refine = facetValue => {
const refinedState = refine(
helper.state,
- attributeName,
- options,
+ attribute,
+ items,
facetValue
);
helper.setState(refinedState).search();
};
this._createURL = state => facetValue =>
- createURL(refine(state, attributeName, options, facetValue));
+ createURL(refine(state, attribute, items, facetValue));
this._prepareItems = state =>
- options.map(({ start, end, name: label }) => ({
+ items.map(({ start, end, label }) => ({
label,
value: window.encodeURI(JSON.stringify({ start, end })),
- isRefined: isRefined(state, attributeName, { start, end }),
+ isRefined: isRefined(state, attribute, { start, end }),
}));
renderFn(
@@ -177,20 +173,20 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
dispose({ state }) {
unmountFn();
- return state.clearRefinements(attributeName);
+ return state.clearRefinements(attribute);
},
getWidgetState(uiState, { searchParameters }) {
const currentRefinements = searchParameters.getNumericRefinements(
- attributeName
+ attribute
);
const equal = currentRefinements['='] && currentRefinements['='][0];
if (equal || equal === 0) {
return {
...uiState,
- numericRefinementList: {
- ...uiState.numericRefinementList,
- [attributeName]: `${currentRefinements['=']}`,
+ numericMenu: {
+ ...uiState.numericMenu,
+ [attribute]: `${currentRefinements['=']}`,
},
};
}
@@ -202,16 +198,15 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
if (lowerBound !== '' || upperBound !== '') {
if (
- uiState.numericRefinementList &&
- uiState.numericRefinementList[attributeName] ===
- `${lowerBound}:${upperBound}`
+ uiState.numericMenu &&
+ uiState.numericMenu[attribute] === `${lowerBound}:${upperBound}`
)
return uiState;
return {
...uiState,
- numericRefinementList: {
- ...uiState.numericRefinementList,
- [attributeName]: `${lowerBound}:${upperBound}`,
+ numericMenu: {
+ ...uiState.numericMenu,
+ [attribute]: `${lowerBound}:${upperBound}`,
},
};
}
@@ -220,10 +215,8 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
},
getWidgetSearchParameters(searchParameters, { uiState }) {
- let clearedParams = searchParameters.clearRefinements(attributeName);
- const value =
- uiState.numericRefinementList &&
- uiState.numericRefinementList[attributeName];
+ let clearedParams = searchParameters.clearRefinements(attribute);
+ const value = uiState.numericMenu && uiState.numericMenu[attribute];
if (!value) {
return clearedParams;
@@ -233,7 +226,7 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
if (valueAsEqual) {
return clearedParams.addNumericRefinement(
- attributeName,
+ attribute,
'=',
valueAsEqual
);
@@ -243,7 +236,7 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
if (_isFinite(lowerBound)) {
clearedParams = clearedParams.addNumericRefinement(
- attributeName,
+ attribute,
'>=',
lowerBound
);
@@ -251,7 +244,7 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
if (_isFinite(upperBound)) {
clearedParams = clearedParams.addNumericRefinement(
- attributeName,
+ attribute,
'<=',
upperBound
);
@@ -263,8 +256,8 @@ export default function connectNumericRefinementList(renderFn, unmountFn) {
};
}
-function isRefined(state, attributeName, option) {
- const currentRefinements = state.getNumericRefinements(attributeName);
+function isRefined(state, attribute, option) {
+ const currentRefinements = state.getNumericRefinements(attribute);
if (option.start !== undefined && option.end !== undefined) {
if (option.start === option.end) {
@@ -287,19 +280,19 @@ function isRefined(state, attributeName, option) {
return undefined;
}
-function refine(state, attributeName, options, facetValue) {
+function refine(state, attribute, items, facetValue) {
let resolvedState = state;
const refinedOption = JSON.parse(window.decodeURI(facetValue));
- const currentRefinements = resolvedState.getNumericRefinements(attributeName);
+ const currentRefinements = resolvedState.getNumericRefinements(attribute);
if (refinedOption.start === undefined && refinedOption.end === undefined) {
- return resolvedState.clearRefinements(attributeName);
+ return resolvedState.clearRefinements(attribute);
}
- if (!isRefined(resolvedState, attributeName, refinedOption)) {
- resolvedState = resolvedState.clearRefinements(attributeName);
+ if (!isRefined(resolvedState, attribute, refinedOption)) {
+ resolvedState = resolvedState.clearRefinements(attribute);
}
if (refinedOption.start !== undefined && refinedOption.end !== undefined) {
@@ -310,13 +303,13 @@ function refine(state, attributeName, options, facetValue) {
if (refinedOption.start === refinedOption.end) {
if (hasNumericRefinement(currentRefinements, '=', refinedOption.start)) {
resolvedState = resolvedState.removeNumericRefinement(
- attributeName,
+ attribute,
'=',
refinedOption.start
);
} else {
resolvedState = resolvedState.addNumericRefinement(
- attributeName,
+ attribute,
'=',
refinedOption.start
);
@@ -328,13 +321,13 @@ function refine(state, attributeName, options, facetValue) {
if (refinedOption.start !== undefined) {
if (hasNumericRefinement(currentRefinements, '>=', refinedOption.start)) {
resolvedState = resolvedState.removeNumericRefinement(
- attributeName,
+ attribute,
'>=',
refinedOption.start
);
} else {
resolvedState = resolvedState.addNumericRefinement(
- attributeName,
+ attribute,
'>=',
refinedOption.start
);
@@ -344,13 +337,13 @@ function refine(state, attributeName, options, facetValue) {
if (refinedOption.end !== undefined) {
if (hasNumericRefinement(currentRefinements, '<=', refinedOption.end)) {
resolvedState = resolvedState.removeNumericRefinement(
- attributeName,
+ attribute,
'<=',
refinedOption.end
);
} else {
resolvedState = resolvedState.addNumericRefinement(
- attributeName,
+ attribute,
'<=',
refinedOption.end
);
diff --git a/src/connectors/numeric-refinement-list/__tests__/__snapshots__/connectNumericRefinementList-test.js.snap b/src/connectors/numeric-refinement-list/__tests__/__snapshots__/connectNumericRefinementList-test.js.snap
deleted file mode 100644
index f19c6ec1c9..0000000000
--- a/src/connectors/numeric-refinement-list/__tests__/__snapshots__/connectNumericRefinementList-test.js.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`connectNumericRefinementList routing getWidgetState should add an entry equal to the refinement (equal) 1`] = `
-Object {
- "numericRefinementList": Object {
- "numerics": "20",
- },
-}
-`;
-
-exports[`connectNumericRefinementList routing getWidgetState should add an entry equal to the refinement (only max) 1`] = `
-Object {
- "numericRefinementList": Object {
- "numerics": ":20",
- },
-}
-`;
-
-exports[`connectNumericRefinementList routing getWidgetState should add an entry equal to the refinement (only min) 1`] = `
-Object {
- "numericRefinementList": Object {
- "numerics": "10:",
- },
-}
-`;
-
-exports[`connectNumericRefinementList routing getWidgetState should add an entry equal to the refinement (range) 1`] = `
-Object {
- "numericRefinementList": Object {
- "numerics": "10:20",
- },
-}
-`;
-
-exports[`connectNumericRefinementList routing getWidgetState should not override other values in the same namespace 1`] = `
-Object {
- "numericRefinementList": Object {
- "numerics": "10:20",
- "numerics-2": "27:36",
- },
-}
-`;
diff --git a/src/connectors/numeric-selector/__tests__/__snapshots__/connectNumericSelector-test.js.snap b/src/connectors/numeric-selector/__tests__/__snapshots__/connectNumericSelector-test.js.snap
deleted file mode 100644
index 1e269defb9..0000000000
--- a/src/connectors/numeric-selector/__tests__/__snapshots__/connectNumericSelector-test.js.snap
+++ /dev/null
@@ -1,144 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`connectNumericSelector routing getWidgetSearchParameters should add the refinements according to the UI state provided 1`] = `
-SearchParameters {
- "advancedSyntax": undefined,
- "allowTyposOnNumericTokens": undefined,
- "analytics": undefined,
- "analyticsTags": undefined,
- "aroundLatLng": undefined,
- "aroundLatLngViaIP": undefined,
- "aroundPrecision": undefined,
- "aroundRadius": undefined,
- "attributesToHighlight": undefined,
- "attributesToRetrieve": undefined,
- "attributesToSnippet": undefined,
- "disableExactOnAttributes": undefined,
- "disjunctiveFacets": Array [],
- "disjunctiveFacetsRefinements": Object {},
- "distinct": undefined,
- "enableExactOnSingleWordQuery": undefined,
- "facets": Array [],
- "facetsExcludes": Object {},
- "facetsRefinements": Object {},
- "getRankingInfo": undefined,
- "hierarchicalFacets": Array [],
- "hierarchicalFacetsRefinements": Object {},
- "highlightPostTag": undefined,
- "highlightPreTag": undefined,
- "hitsPerPage": undefined,
- "ignorePlurals": undefined,
- "index": "",
- "insideBoundingBox": undefined,
- "insidePolygon": undefined,
- "length": undefined,
- "maxValuesPerFacet": undefined,
- "minProximity": undefined,
- "minWordSizefor1Typo": undefined,
- "minWordSizefor2Typos": undefined,
- "minimumAroundRadius": undefined,
- "numericFilters": undefined,
- "numericRefinements": Object {
- "numerics": Object {
- "=": Array [
- 20,
- ],
- },
- },
- "offset": undefined,
- "optionalFacetFilters": undefined,
- "optionalTagFilters": undefined,
- "optionalWords": undefined,
- "page": 0,
- "query": "",
- "queryType": undefined,
- "removeWordsIfNoResults": undefined,
- "replaceSynonymsInHighlight": undefined,
- "restrictSearchableAttributes": undefined,
- "snippetEllipsisText": undefined,
- "synonyms": undefined,
- "tagFilters": undefined,
- "tagRefinements": Array [],
- "typoTolerance": undefined,
-}
-`;
-
-exports[`connectNumericSelector routing getWidgetSearchParameters should enforce the default value if there are no refinements in the UI state 1`] = `
-SearchParameters {
- "advancedSyntax": undefined,
- "allowTyposOnNumericTokens": undefined,
- "analytics": undefined,
- "analyticsTags": undefined,
- "aroundLatLng": undefined,
- "aroundLatLngViaIP": undefined,
- "aroundPrecision": undefined,
- "aroundRadius": undefined,
- "attributesToHighlight": undefined,
- "attributesToRetrieve": undefined,
- "attributesToSnippet": undefined,
- "disableExactOnAttributes": undefined,
- "disjunctiveFacets": Array [],
- "disjunctiveFacetsRefinements": Object {},
- "distinct": undefined,
- "enableExactOnSingleWordQuery": undefined,
- "facets": Array [],
- "facetsExcludes": Object {},
- "facetsRefinements": Object {},
- "getRankingInfo": undefined,
- "hierarchicalFacets": Array [],
- "hierarchicalFacetsRefinements": Object {},
- "highlightPostTag": undefined,
- "highlightPreTag": undefined,
- "hitsPerPage": undefined,
- "ignorePlurals": undefined,
- "index": "",
- "insideBoundingBox": undefined,
- "insidePolygon": undefined,
- "length": undefined,
- "maxValuesPerFacet": undefined,
- "minProximity": undefined,
- "minWordSizefor1Typo": undefined,
- "minWordSizefor2Typos": undefined,
- "minimumAroundRadius": undefined,
- "numericFilters": undefined,
- "numericRefinements": Object {
- "numerics": Object {
- "=": Array [
- 10,
- ],
- },
- },
- "offset": undefined,
- "optionalFacetFilters": undefined,
- "optionalTagFilters": undefined,
- "optionalWords": undefined,
- "page": 0,
- "query": "",
- "queryType": undefined,
- "removeWordsIfNoResults": undefined,
- "replaceSynonymsInHighlight": undefined,
- "restrictSearchableAttributes": undefined,
- "snippetEllipsisText": undefined,
- "synonyms": undefined,
- "tagFilters": undefined,
- "tagRefinements": Array [],
- "typoTolerance": undefined,
-}
-`;
-
-exports[`connectNumericSelector routing getWidgetState should add an entry equal to the refinement 1`] = `
-Object {
- "numericSelector": Object {
- "numerics": 20,
- },
-}
-`;
-
-exports[`connectNumericSelector routing getWidgetState should not override other values in the same namespace 1`] = `
-Object {
- "numericSelector": Object {
- "numerics": 20,
- "numerics-2": "36",
- },
-}
-`;
diff --git a/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js b/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js
deleted file mode 100644
index beb3e9dbde..0000000000
--- a/src/connectors/numeric-selector/__tests__/connectNumericSelector-test.js
+++ /dev/null
@@ -1,481 +0,0 @@
-import jsHelper, {
- SearchResults,
- SearchParameters,
-} from 'algoliasearch-helper';
-
-import connectNumericSelector from '../connectNumericSelector.js';
-
-describe('connectNumericSelector', () => {
- it('Renders during init and render', () => {
- // test that the dummyRendering is called with the isFirstRendering
- // flag set accordingly
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- const config = widget.getConfiguration({}, {});
- expect(config).toEqual({
- numericRefinements: {
- numerics: {
- '=': [listOptions[0].value],
- },
- },
- });
-
- // test if widget is not rendered yet at this point
- expect(rendering).not.toHaveBeenCalled();
-
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- // test that rendering has been called during init with isFirstRendering = true
- expect(rendering).toHaveBeenCalledTimes(1);
- // test if isFirstRendering is true during init
- expect(rendering).toHaveBeenLastCalledWith(expect.any(Object), true);
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- expect(firstRenderingOptions.currentRefinement).toBe(listOptions[0].value);
- expect(firstRenderingOptions.widgetParams).toEqual({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- widget.render({
- results: new SearchResults(helper.state, [{ nbHits: 0 }]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- // test that rendering has been called during init with isFirstRendering = false
- expect(rendering).toHaveBeenCalledTimes(2);
- expect(rendering).toHaveBeenLastCalledWith(expect.any(Object), false);
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- expect(secondRenderingOptions.currentRefinement).toBe(listOptions[0].value);
- expect(secondRenderingOptions.widgetParams).toEqual({
- attributeName: 'numerics',
- options: listOptions,
- });
- });
-
- it('Renders during init and render with transformed items', () => {
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- transformItems: items =>
- items.map(item => ({ ...item, label: 'transformed' })),
- });
-
- const config = widget.getConfiguration({}, {});
-
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- expect(firstRenderingOptions.options).toEqual([
- { name: '10', value: 10, label: 'transformed' },
- { name: '20', value: 20, label: 'transformed' },
- { name: '30', value: 30, label: 'transformed' },
- ]);
-
- widget.render({
- results: new SearchResults(helper.state, [{ nbHits: 0 }]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- expect(secondRenderingOptions.options).toEqual([
- { name: '10', value: 10, label: 'transformed' },
- { name: '20', value: 20, label: 'transformed' },
- { name: '30', value: 30, label: 'transformed' },
- ]);
- });
-
- it('Reads the default value from the URL if possible', () => {
- // test that the dummyRendering is called with the isFirstRendering
- // flag set accordingly
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- expect(widget.getConfiguration({}, {})).toEqual({
- numericRefinements: {
- numerics: {
- '=': [listOptions[0].value],
- },
- },
- });
-
- expect(
- widget.getConfiguration(
- {},
- {
- numericRefinements: {
- numerics: {
- '=': [30],
- },
- },
- }
- )
- ).toEqual({
- numericRefinements: {
- numerics: {
- '=': [30],
- },
- },
- });
- });
-
- it('Provide a function to update the refinements at each step', () => {
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- const config = widget.getConfiguration({}, {});
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const { refine } = firstRenderingOptions;
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
- refine(listOptions[1].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [20],
- });
- refine(listOptions[2].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [30],
- });
- refine(listOptions[0].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- const { refine: renderSetValue } = secondRenderingOptions;
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
- renderSetValue(listOptions[1].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [20],
- });
- renderSetValue(listOptions[2].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [30],
- });
- renderSetValue(listOptions[0].name);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
- });
-
- it('provides isRefined for the currently selected value', () => {
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- const config = widget.getConfiguration({}, {});
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- let refine = rendering.mock.calls[0][0].refine;
-
- listOptions.forEach((_, i) => {
- // we loop with 1 increment because the first value is selected by default
- const currentOption = listOptions[(i + 1) % listOptions.length];
- refine(currentOption.name);
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- // The current option should be the one selected
- // First we copy and set the default added values
- const expectedResult = currentOption.value;
-
- const renderingParameters = rendering.mock.calls[1 + i][0];
- expect(renderingParameters.currentRefinement).toEqual(expectedResult);
-
- refine = renderingParameters.refine;
- });
- });
-
- it('The refine function can unselect with `undefined` and "undefined"', () => {
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '' },
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- const config = widget.getConfiguration({}, {});
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const firstRenderingOptions = rendering.mock.calls[0][0];
- const { refine } = firstRenderingOptions;
- expect(helper.state.getNumericRefinements('numerics')).toEqual({});
- refine(listOptions[1].value);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
- refine(listOptions[0].value);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({});
-
- widget.render({
- results: new SearchResults(helper.state, [{}]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- const secondRenderingOptions = rendering.mock.calls[1][0];
- const { refine: refineBis } = secondRenderingOptions;
- expect(helper.state.getNumericRefinements('numerics')).toEqual({});
- refineBis(listOptions[1].value);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({
- '=': [10],
- });
- refineBis(listOptions[0].value);
- expect(helper.state.getNumericRefinements('numerics')).toEqual({});
- });
-
- describe('routing', () => {
- const getInitializedWidget = () => {
- const rendering = jest.fn();
- const makeWidget = connectNumericSelector(rendering);
- const listOptions = [
- { name: '10', value: 10 },
- { name: '20', value: 20 },
- { name: '30', value: 30 },
- ];
- const widget = makeWidget({
- attributeName: 'numerics',
- options: listOptions,
- });
-
- const config = widget.getConfiguration({}, {});
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const { refine } = rendering.mock.calls[0][0];
-
- return [widget, helper, refine];
- };
-
- describe('getWidgetState', () => {
- test('should give back the object unmodified if the default value is selected', () => {
- const [widget, helper] = getInitializedWidget();
- const uiStateBefore = {};
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toBe(uiStateBefore);
- });
-
- test('should add an entry equal to the refinement', () => {
- const [widget, helper, refine] = getInitializedWidget();
- refine(20);
- const uiStateBefore = {};
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toMatchSnapshot();
- });
-
- test('should not override other values in the same namespace', () => {
- const [widget, helper, refine] = getInitializedWidget();
- const uiStateBefore = {
- numericSelector: {
- 'numerics-2': '36',
- },
- };
-
- refine(20);
-
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toMatchSnapshot();
- });
-
- test('should give back the object unmodified if refinements are already set', () => {
- const [widget, helper] = getInitializedWidget();
- const uiStateBefore = {
- numericSelector: {
- numerics: 20,
- },
- };
- helper.addNumericRefinement('numerics', '=', 20);
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toBe(uiStateBefore);
- });
- });
-
- describe('getWidgetSearchParameters', () => {
- test('should enforce the default value if there are no refinements in the UI state', () => {
- const [widget, helper] = getInitializedWidget();
- // User presses back (browser) and the URL contains nothing
- const uiState = {};
- // The current search is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // The default parameters should be applied
- expect(searchParametersAfter).toMatchSnapshot();
- });
-
- test('should return the same SP if the value is the same in both UI State and SP', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // User presses back (browser) and the URL contains some refinements
- const uiState = {
- numericSelector: {
- numerics: 30,
- },
- };
- // The current state has the same parameters
- refine(30);
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same parameters should not return a new object
- expect(searchParametersAfter).toBe(searchParametersBefore);
- });
-
- test('should add the refinements according to the UI state provided', () => {
- const [widget, helper] = getInitializedWidget();
- // User presses back (browser) and the URL contains some refinements
- const uiState = {
- numericSelector: {
- numerics: 20,
- },
- };
- // The current state is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // The new parameters should be applies
- expect(searchParametersAfter).toMatchSnapshot();
- });
- });
- });
-});
diff --git a/src/connectors/numeric-selector/connectNumericSelector.js b/src/connectors/numeric-selector/connectNumericSelector.js
deleted file mode 100644
index 7e3f169402..0000000000
--- a/src/connectors/numeric-selector/connectNumericSelector.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import { checkRendering } from '../../lib/utils.js';
-
-const usage = `Usage:
-var customNumericSelector = connectNumericSelector(function renderFn(params, isFirstRendering) {
- // params = {
- // currentRefinement,
- // options,
- // refine,
- // hasNoResults,
- // instantSearchInstance,
- // widgetParams,
- // }
-});
-search.addWidget(
- customNumericSelector({
- attributeName,
- options,
- [ operator = '=' ],
- [ transformItems ]
- })
-);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectNumericSelector.html
-`;
-
-/**
- * @typedef {Object} NumericSelectorOption
- * @property {number} value The numerical value to refine with.
- * If the value is `undefined` or `"undefined"`, the option resets the filter.
- * @property {string} label Label to display in the option.
- */
-
-/**
- * @typedef {Object} CustomNumericSelectorWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
- * @property {NumericSelectorOption[]} options Array of objects defining the different values and labels.
- * @property {string} [operator = '='] The operator to use to refine. Supports following operators: <, <=, =, >, >= and !=.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * @typedef {Object} NumericSelectorRenderingOptions
- * @property {string} currentRefinement The currently selected value.
- * @property {NumericSelectorOption[]} options The different values and labels of the selector.
- * @property {function(option.value)} refine Updates the results with the selected value.
- * @property {boolean} hasNoResults `true` if the last search contains no result.
- * @property {Object} widgetParams All original `CustomNumericSelectorWidgetOptions` forwarded to the `renderFn`.
- */
-
-/**
- * **NumericSelector** connector provides the logic to build a custom widget that will let the
- * user filter the results based on a list of numerical filters.
- *
- * It provides a `refine(value)` function to trigger a new search with selected option.
- * @type {Connector}
- * @param {function(NumericSelectorRenderingOptions, boolean)} renderFn Rendering function for the custom **NumericSelector** widget.
- * @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomNumericSelectorWidgetOptions)} Re-usable widget factory for a custom **NumericSelector** widget.
- * @example
- * // custom `renderFn` to render the custom NumericSelector widget
- * function renderFn(NumericSelectorRenderingOptions, isFirstRendering) {
- * if (isFirstRendering) {
- * NumericSelectorRenderingOptions.widgetParams.containerNode.html(' ');
- * NumericSelectorRenderingOptions.widgetParams.containerNode
- * .find('select')
- * .on('change', function(event) {
- * NumericSelectorRenderingOptions.refine(event.target.value);
- * })
- * }
- *
- * var optionsHTML = NumericSelectorRenderingOptions.options.map(function(option) {
- * return '' +
- * option.label + ' ';
- * });
- *
- * NumericSelectorRenderingOptions.widgetParams.containerNode
- * .find('select')
- * .html(optionsHTML);
- * }
- *
- * // connect `renderFn` to NumericSelector logic
- * var customNumericSelector = instantsearch.connectors.connectNumericSelector(renderFn);
- *
- * // mount widget on the page
- * search.addWidget(
- * customNumericSelector({
- * containerNode: $('#custom-numeric-selector-container'),
- * operator: '>=',
- * attributeName: 'popularity',
- * options: [
- * {label: 'Default', value: 0},
- * {label: 'Top 10', value: 9991},
- * {label: 'Top 100', value: 9901},
- * {label: 'Top 500', value: 9501},
- * ],
- * })
- * );
- */
-export default function connectNumericSelector(renderFn, unmountFn) {
- checkRendering(renderFn, usage);
-
- return (widgetParams = {}) => {
- const {
- attributeName,
- options,
- operator = '=',
- transformItems = items => items,
- } = widgetParams;
-
- if (!attributeName || !options) {
- throw new Error(usage);
- }
-
- return {
- getConfiguration(currentSearchParameters, searchParametersFromUrl) {
- const value = this._getRefinedValue(searchParametersFromUrl);
- if (value) {
- return {
- numericRefinements: {
- [attributeName]: {
- [operator]: [value],
- },
- },
- };
- }
- return {};
- },
-
- init({ helper, instantSearchInstance }) {
- this._refine = value => {
- helper.clearRefinements(attributeName);
- if (value !== undefined && value !== 'undefined') {
- helper.addNumericRefinement(attributeName, operator, value);
- }
- helper.search();
- };
-
- renderFn(
- {
- currentRefinement: this._getRefinedValue(helper.state),
- options: transformItems(options),
- refine: this._refine,
- hasNoResults: true,
- instantSearchInstance,
- widgetParams,
- },
- true
- );
- },
-
- render({ helper, results, instantSearchInstance }) {
- renderFn(
- {
- currentRefinement: this._getRefinedValue(helper.state),
- options: transformItems(options),
- refine: this._refine,
- hasNoResults: results.nbHits === 0,
- instantSearchInstance,
- widgetParams,
- },
- false
- );
- },
-
- dispose({ state }) {
- unmountFn();
- return state.removeNumericRefinement(attributeName);
- },
-
- getWidgetState(uiState, { searchParameters }) {
- const currentRefinement = this._getRefinedValue(searchParameters);
- if (
- // Does the current state contain the current refinement?
- (uiState.numericSelector &&
- currentRefinement === uiState.numericSelector[attributeName]) ||
- // Is the current value the first option / default value?
- currentRefinement === options[0].value
- ) {
- return uiState;
- }
-
- if (currentRefinement || currentRefinement === 0)
- return {
- ...uiState,
- numericSelector: {
- ...uiState.numericSelector,
- [attributeName]: currentRefinement,
- },
- };
- return uiState;
- },
-
- getWidgetSearchParameters(searchParameters, { uiState }) {
- const value =
- uiState.numericSelector && uiState.numericSelector[attributeName];
- const currentlyRefinedValue = this._getRefinedValue(searchParameters);
-
- if (value) {
- if (value === currentlyRefinedValue) return searchParameters;
- return searchParameters
- .clearRefinements(attributeName)
- .addNumericRefinement(attributeName, operator, value);
- }
-
- const firstItemValue = options[0] && options[0].value;
- if (typeof firstItemValue === 'number') {
- return searchParameters
- .clearRefinements(attributeName)
- .addNumericRefinement(attributeName, operator, options[0].value);
- }
-
- return searchParameters;
- },
-
- _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;
- },
- };
- };
-}
diff --git a/src/connectors/pagination/__tests__/connectPagination-test.js b/src/connectors/pagination/__tests__/connectPagination-test.js
index d69388289b..d746d17a35 100644
--- a/src/connectors/pagination/__tests__/connectPagination-test.js
+++ b/src/connectors/pagination/__tests__/connectPagination-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
@@ -11,7 +9,7 @@ describe('connectPagination', () => {
it('connectPagination - Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectPagination(rendering);
const widget = makeWidget({
foo: 'bar', // dummy param for `widgetParams` test
@@ -21,7 +19,7 @@ describe('connectPagination', () => {
expect(widget.getConfiguration).toBe(undefined);
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -32,12 +30,14 @@ describe('connectPagination', () => {
{
// should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(1);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(true);
// should provide good values for the first rendering
- const firstRenderingOptions = rendering.lastCall.args[0];
+ const firstRenderingOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(firstRenderingOptions.currentRefinement).toBe(0);
expect(firstRenderingOptions.nbHits).toBe(0);
expect(firstRenderingOptions.nbPages).toBe(0);
@@ -62,12 +62,14 @@ describe('connectPagination', () => {
{
// Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(2);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(false);
// should call the rendering with values from the results
- const secondRenderingOptions = rendering.lastCall.args[0];
+ const secondRenderingOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(secondRenderingOptions.currentRefinement).toBe(0);
expect(secondRenderingOptions.nbHits).toBe(1);
expect(secondRenderingOptions.nbPages).toBe(1);
@@ -75,13 +77,13 @@ describe('connectPagination', () => {
});
it('Provides a function to update the refinements at each step', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectPagination(rendering);
const widget = makeWidget();
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -92,11 +94,12 @@ describe('connectPagination', () => {
{
// first rendering
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine(2);
expect(helper.getPage()).toBe(2);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
}
widget.render({
@@ -108,11 +111,12 @@ describe('connectPagination', () => {
{
// Second rendering
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine(7);
expect(helper.getPage()).toBe(7);
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
}
});
diff --git a/src/connectors/pagination/connectPagination.js b/src/connectors/pagination/connectPagination.js
index 9598eb408c..67d59ea6f6 100644
--- a/src/connectors/pagination/connectPagination.js
+++ b/src/connectors/pagination/connectPagination.js
@@ -15,7 +15,7 @@ var customPagination = connectPagination(function render(params, isFirstRenderin
});
search.addWidget(
customPagination({
- [ maxPages ]
+ [ totalPages ]
[ padding ]
})
);
@@ -24,8 +24,8 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomPaginationWidgetOptions
- * @property {number} [maxPages] The max number of pages to browse.
- * @property {number} [padding=3] The padding of pages to show around the current page
+ * @property {number} [totalPages] The total number of pages to browse.
+ * @property {number} [padding = 3] The padding of pages to show around the current page
*/
/**
@@ -92,7 +92,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* search.addWidget(
* customPagination({
* containerNode: $('#custom-pagination-container'),
- * maxPages: 20,
+ * totalPages: 20,
* padding: 4,
* })
* );
@@ -101,7 +101,7 @@ export default function connectPagination(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { maxPages, padding = 3 } = widgetParams;
+ const { totalPages, padding = 3 } = widgetParams;
const pager = new Paginator({
currentPage: 0,
@@ -136,7 +136,9 @@ export default function connectPagination(renderFn, unmountFn) {
},
getMaxPage({ nbPages }) {
- return maxPages !== undefined ? Math.min(maxPages, nbPages) : nbPages;
+ return totalPages !== undefined
+ ? Math.min(totalPages, nbPages)
+ : nbPages;
},
render({ results, state, instantSearchInstance }) {
diff --git a/src/connectors/powered-by/__tests__/connectPoweredBy-test.js b/src/connectors/powered-by/__tests__/connectPoweredBy-test.js
new file mode 100644
index 0000000000..ad68c6c4c3
--- /dev/null
+++ b/src/connectors/powered-by/__tests__/connectPoweredBy-test.js
@@ -0,0 +1,84 @@
+import connectPoweredBy from '../connectPoweredBy';
+
+describe('connectPoweredBy', () => {
+ it('renders during init and render', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectPoweredBy(rendering);
+ const widget = makeWidget();
+
+ // does not have a getConfiguration method
+ expect(widget.getConfiguration).toBe(undefined);
+
+ widget.init({});
+
+ expect(rendering).toHaveBeenCalledTimes(1);
+ expect(rendering).toHaveBeenCalledWith(expect.anything(), true);
+
+ widget.render({});
+
+ expect(rendering).toHaveBeenCalledTimes(2);
+ expect(rendering).toHaveBeenLastCalledWith(expect.anything(), false);
+ });
+
+ it('has a default URL at init', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectPoweredBy(rendering);
+ const widget = makeWidget();
+
+ widget.init({});
+
+ expect(rendering).toHaveBeenCalledWith(
+ expect.objectContaining({
+ url:
+ 'https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby',
+ }),
+ true
+ );
+ });
+
+ it('has a default URL at render', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectPoweredBy(rendering);
+ const widget = makeWidget();
+
+ widget.render({});
+
+ expect(rendering).toHaveBeenCalledWith(
+ expect.objectContaining({
+ url:
+ 'https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=localhost&utm_campaign=poweredby',
+ }),
+ false
+ );
+ });
+
+ it('can override the URL', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectPoweredBy(rendering);
+ const widget = makeWidget({
+ url: '#custom-url',
+ });
+
+ widget.init({});
+
+ expect(rendering).toHaveBeenCalledWith(
+ expect.objectContaining({ url: '#custom-url' }),
+ true
+ );
+ });
+
+ it('can override the theme', () => {
+ const rendering = jest.fn();
+ const makeWidget = connectPoweredBy(rendering);
+ const widget = makeWidget({
+ theme: 'dark',
+ });
+
+ widget.init({});
+
+ expect(rendering).toHaveBeenCalledWith(
+ expect.objectContaining({ widgetParams: { theme: 'dark' } }),
+ true
+ );
+ });
+});
diff --git a/src/connectors/powered-by/connectPoweredBy.js b/src/connectors/powered-by/connectPoweredBy.js
new file mode 100644
index 0000000000..cff6b054d6
--- /dev/null
+++ b/src/connectors/powered-by/connectPoweredBy.js
@@ -0,0 +1,78 @@
+import { checkRendering } from '../../lib/utils.js';
+
+const usage = `Usage:
+var customPoweredBy = connectPoweredBy(function render(params, isFirstRendering) {
+ // params = {
+ // url,
+ // widgetParams,
+ // }
+});
+search.addWidget(customPoweredBy({
+ [ url ],
+}));
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectPoweredBy.html`;
+
+/**
+ * @typedef {Object} PoweredByWidgetOptions
+ * @property {string} [theme] The theme of the logo ("light" or "dark").
+ * @property {string} [url] The URL to redirect to.
+ */
+
+/**
+ * @typedef {Object} PoweredByRenderingOptions
+ * @property {Object} widgetParams All original `PoweredByWidgetOptions` forwarded to the `renderFn`.
+ */
+
+/**
+ * **PoweredBy** connector provides the logic to build a custom widget that will displays
+ * the logo to redirect to Algolia.
+ *
+ * @type {Connector}
+ * @param {function(PoweredByRenderingOptions, boolean)} renderFn Rendering function for the custom **PoweredBy** widget.
+ * @param {function} unmountFn Unmount function called when the widget is disposed.
+ * @return {function} Re-usable widget factory for a custom **PoweredBy** widget.
+ */
+export default function connectPoweredBy(renderFn, unmountFn) {
+ checkRendering(renderFn, usage);
+
+ const defaultUrl =
+ 'https://www.algolia.com/?' +
+ 'utm_source=instantsearch.js&' +
+ 'utm_medium=website&' +
+ `utm_content=${
+ typeof window !== 'undefined' && window.location
+ ? window.location.hostname
+ : ''
+ }&` +
+ 'utm_campaign=poweredby';
+
+ return (widgetParams = {}) => {
+ const { url = defaultUrl } = widgetParams;
+
+ return {
+ init() {
+ renderFn(
+ {
+ url,
+ widgetParams,
+ },
+ true
+ );
+ },
+
+ render() {
+ renderFn(
+ {
+ url,
+ widgetParams,
+ },
+ false
+ );
+ },
+
+ dispose() {
+ unmountFn();
+ },
+ };
+ };
+}
diff --git a/src/connectors/price-ranges/__tests__/__snapshots__/connectPriceRanges-test.js.snap b/src/connectors/price-ranges/__tests__/__snapshots__/connectPriceRanges-test.js.snap
deleted file mode 100644
index 3778960ebf..0000000000
--- a/src/connectors/price-ranges/__tests__/__snapshots__/connectPriceRanges-test.js.snap
+++ /dev/null
@@ -1,216 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`connectPriceRanges routing getWidgetSearchParameters should add the refinements according to the UI state provided (min and max) 1`] = `
-SearchParameters {
- "advancedSyntax": undefined,
- "allowTyposOnNumericTokens": undefined,
- "analytics": undefined,
- "analyticsTags": undefined,
- "aroundLatLng": undefined,
- "aroundLatLngViaIP": undefined,
- "aroundPrecision": undefined,
- "aroundRadius": undefined,
- "attributesToHighlight": undefined,
- "attributesToRetrieve": undefined,
- "attributesToSnippet": undefined,
- "disableExactOnAttributes": undefined,
- "disjunctiveFacets": Array [],
- "disjunctiveFacetsRefinements": Object {},
- "distinct": undefined,
- "enableExactOnSingleWordQuery": undefined,
- "facets": Array [
- "price",
- ],
- "facetsExcludes": Object {},
- "facetsRefinements": Object {},
- "getRankingInfo": undefined,
- "hierarchicalFacets": Array [],
- "hierarchicalFacetsRefinements": Object {},
- "highlightPostTag": undefined,
- "highlightPreTag": undefined,
- "hitsPerPage": undefined,
- "ignorePlurals": undefined,
- "index": "",
- "insideBoundingBox": undefined,
- "insidePolygon": undefined,
- "length": undefined,
- "maxValuesPerFacet": undefined,
- "minProximity": undefined,
- "minWordSizefor1Typo": undefined,
- "minWordSizefor2Typos": undefined,
- "minimumAroundRadius": undefined,
- "numericFilters": undefined,
- "numericRefinements": Object {
- "price": Object {
- "<=": Array [
- 40,
- ],
- ">=": Array [
- 20,
- ],
- },
- },
- "offset": undefined,
- "optionalFacetFilters": undefined,
- "optionalTagFilters": undefined,
- "optionalWords": undefined,
- "page": 0,
- "query": "",
- "queryType": undefined,
- "removeWordsIfNoResults": undefined,
- "replaceSynonymsInHighlight": undefined,
- "restrictSearchableAttributes": undefined,
- "snippetEllipsisText": undefined,
- "synonyms": undefined,
- "tagFilters": undefined,
- "tagRefinements": Array [],
- "typoTolerance": undefined,
-}
-`;
-
-exports[`connectPriceRanges routing getWidgetSearchParameters should add the refinements according to the UI state provided (only max) 1`] = `
-SearchParameters {
- "advancedSyntax": undefined,
- "allowTyposOnNumericTokens": undefined,
- "analytics": undefined,
- "analyticsTags": undefined,
- "aroundLatLng": undefined,
- "aroundLatLngViaIP": undefined,
- "aroundPrecision": undefined,
- "aroundRadius": undefined,
- "attributesToHighlight": undefined,
- "attributesToRetrieve": undefined,
- "attributesToSnippet": undefined,
- "disableExactOnAttributes": undefined,
- "disjunctiveFacets": Array [],
- "disjunctiveFacetsRefinements": Object {},
- "distinct": undefined,
- "enableExactOnSingleWordQuery": undefined,
- "facets": Array [
- "price",
- ],
- "facetsExcludes": Object {},
- "facetsRefinements": Object {},
- "getRankingInfo": undefined,
- "hierarchicalFacets": Array [],
- "hierarchicalFacetsRefinements": Object {},
- "highlightPostTag": undefined,
- "highlightPreTag": undefined,
- "hitsPerPage": undefined,
- "ignorePlurals": undefined,
- "index": "",
- "insideBoundingBox": undefined,
- "insidePolygon": undefined,
- "length": undefined,
- "maxValuesPerFacet": undefined,
- "minProximity": undefined,
- "minWordSizefor1Typo": undefined,
- "minWordSizefor2Typos": undefined,
- "minimumAroundRadius": undefined,
- "numericFilters": undefined,
- "numericRefinements": Object {
- "price": Object {
- "<=": Array [
- 50,
- ],
- },
- },
- "offset": undefined,
- "optionalFacetFilters": undefined,
- "optionalTagFilters": undefined,
- "optionalWords": undefined,
- "page": 0,
- "query": "",
- "queryType": undefined,
- "removeWordsIfNoResults": undefined,
- "replaceSynonymsInHighlight": undefined,
- "restrictSearchableAttributes": undefined,
- "snippetEllipsisText": undefined,
- "synonyms": undefined,
- "tagFilters": undefined,
- "tagRefinements": Array [],
- "typoTolerance": undefined,
-}
-`;
-
-exports[`connectPriceRanges routing getWidgetSearchParameters should add the refinements according to the UI state provided (only min) 1`] = `
-SearchParameters {
- "advancedSyntax": undefined,
- "allowTyposOnNumericTokens": undefined,
- "analytics": undefined,
- "analyticsTags": undefined,
- "aroundLatLng": undefined,
- "aroundLatLngViaIP": undefined,
- "aroundPrecision": undefined,
- "aroundRadius": undefined,
- "attributesToHighlight": undefined,
- "attributesToRetrieve": undefined,
- "attributesToSnippet": undefined,
- "disableExactOnAttributes": undefined,
- "disjunctiveFacets": Array [],
- "disjunctiveFacetsRefinements": Object {},
- "distinct": undefined,
- "enableExactOnSingleWordQuery": undefined,
- "facets": Array [
- "price",
- ],
- "facetsExcludes": Object {},
- "facetsRefinements": Object {},
- "getRankingInfo": undefined,
- "hierarchicalFacets": Array [],
- "hierarchicalFacetsRefinements": Object {},
- "highlightPostTag": undefined,
- "highlightPreTag": undefined,
- "hitsPerPage": undefined,
- "ignorePlurals": undefined,
- "index": "",
- "insideBoundingBox": undefined,
- "insidePolygon": undefined,
- "length": undefined,
- "maxValuesPerFacet": undefined,
- "minProximity": undefined,
- "minWordSizefor1Typo": undefined,
- "minWordSizefor2Typos": undefined,
- "minimumAroundRadius": undefined,
- "numericFilters": undefined,
- "numericRefinements": Object {
- "price": Object {
- ">=": Array [
- 10,
- ],
- },
- },
- "offset": undefined,
- "optionalFacetFilters": undefined,
- "optionalTagFilters": undefined,
- "optionalWords": undefined,
- "page": 0,
- "query": "",
- "queryType": undefined,
- "removeWordsIfNoResults": undefined,
- "replaceSynonymsInHighlight": undefined,
- "restrictSearchableAttributes": undefined,
- "snippetEllipsisText": undefined,
- "synonyms": undefined,
- "tagFilters": undefined,
- "tagRefinements": Array [],
- "typoTolerance": undefined,
-}
-`;
-
-exports[`connectPriceRanges routing getWidgetState should add an entry equal to the refinement 1`] = `
-Object {
- "priceRanges": Object {
- "price": "10:20",
- },
-}
-`;
-
-exports[`connectPriceRanges routing getWidgetState should not override other values in the same namespace 1`] = `
-Object {
- "priceRanges": Object {
- "price": "10:20",
- "price-2": "10:20",
- },
-}
-`;
diff --git a/src/connectors/price-ranges/__tests__/connectPriceRanges-test.js b/src/connectors/price-ranges/__tests__/connectPriceRanges-test.js
deleted file mode 100644
index e81e86881c..0000000000
--- a/src/connectors/price-ranges/__tests__/connectPriceRanges-test.js
+++ /dev/null
@@ -1,423 +0,0 @@
-import sinon from 'sinon';
-
-import jsHelper, {
- SearchResults,
- SearchParameters,
-} from 'algoliasearch-helper';
-
-import connectPriceRanges from '../connectPriceRanges.js';
-
-describe('connectPriceRanges', () => {
- it('Renders during init and render', () => {
- // test that the dummyRendering is called with the isFirstRendering
- // flag set accordingly
- const rendering = sinon.stub();
- const makeWidget = connectPriceRanges(rendering);
-
- const attributeName = 'price';
- const widget = makeWidget({
- attributeName,
- });
-
- // does not have a getConfiguration method
- const config = widget.getConfiguration();
- expect(config).toEqual({ facets: [attributeName] });
-
- const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- {
- // should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
- expect(isFirstRendering).toBe(true);
-
- // should provide good values for the first rendering
- const { items } = rendering.lastCall.args[0];
- expect(items).toEqual([]);
- }
-
- widget.render({
- results: new SearchResults(helper.state, [
- {
- hits: [{ test: 'oneTime' }],
- facets: { price: { 10: 1, 20: 1, 30: 1 } },
- // eslint-disable-next-line
- facets_stats: {
- price: {
- avg: 20,
- max: 30,
- min: 10,
- sum: 60,
- },
- },
- nbHits: 1,
- nbPages: 1,
- page: 0,
- },
- ]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- {
- // Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
- expect(isFirstRendering).toBe(false);
-
- // should provide good values for the first rendering
- const { items, widgetParams } = rendering.lastCall.args[0];
- expect(items).toEqual([
- { to: 10, url: '#' },
- { from: 10, to: 13, url: '#' },
- { from: 13, to: 16, url: '#' },
- { from: 16, to: 19, url: '#' },
- { from: 19, to: 22, url: '#' },
- { from: 22, to: 25, url: '#' },
- { from: 25, to: 28, url: '#' },
- { from: 28, url: '#' },
- ]);
- expect(widgetParams).toEqual({
- attributeName,
- });
- }
- });
-
- it('Provides a function to update the refinements at each step', () => {
- const rendering = sinon.stub();
- const makeWidget = connectPriceRanges(rendering);
-
- const attributeName = 'price';
- const widget = makeWidget({
- attributeName,
- });
-
- const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- {
- // first rendering
- expect(helper.getNumericRefinement('price', '>=')).toEqual(undefined);
- expect(helper.getNumericRefinement('price', '<=')).toEqual(undefined);
- const renderOptions = rendering.lastCall.args[0];
- const { refine } = renderOptions;
- refine({ from: 10, to: 30 });
- expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
- expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- expect(helper.search.callCount).toBe(1);
- }
-
- widget.render({
- results: new SearchResults(helper.state, [
- {
- hits: [{ test: 'oneTime' }],
- facets: { price: { 10: 1, 20: 1, 30: 1 } },
- // eslint-disable-next-line
- facets_stats: {
- price: {
- avg: 20,
- max: 30,
- min: 10,
- sum: 60,
- },
- },
- nbHits: 1,
- nbPages: 1,
- page: 0,
- },
- ]),
- state: helper.state,
- helper,
- createURL: () => '#',
- });
-
- {
- // Second rendering
- expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
- expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- const renderOptions = rendering.lastCall.args[0];
- const { refine } = renderOptions;
- refine({ from: 40, to: 50 });
- expect(helper.getNumericRefinement('price', '>=')).toEqual([40]);
- expect(helper.getNumericRefinement('price', '<=')).toEqual([50]);
- expect(helper.search.callCount).toBe(2);
- }
- });
-
- describe('routing', () => {
- const getInitializedWidget = () => {
- const rendering = jest.fn();
- const makeWidget = connectPriceRanges(rendering);
- const widget = makeWidget({
- attributeName: 'price',
- });
-
- const config = widget.getConfiguration({}, {});
- const helper = jsHelper({}, '', config);
- helper.search = jest.fn();
-
- widget.init({
- helper,
- state: helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- });
-
- const { refine } = rendering.mock.calls[0][0];
-
- return [widget, helper, refine];
- };
-
- describe('getWidgetState', () => {
- test('should give back the object unmodified if the default value is selected', () => {
- const [widget, helper] = getInitializedWidget();
- const uiStateBefore = {};
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toBe(uiStateBefore);
- });
-
- test('should add an entry equal to the refinement', () => {
- const [widget, helper, refine] = getInitializedWidget();
- refine({ from: 10, to: 20 });
- const uiStateBefore = {};
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toMatchSnapshot();
- });
-
- test('should not override other values in the same namespace', () => {
- const [widget, helper, refine] = getInitializedWidget();
- refine({ from: 10, to: 20 });
-
- const uiStateBefore = {
- priceRanges: {
- 'price-2': '10:20',
- },
- };
-
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toMatchSnapshot();
- });
-
- test('should return the same instance if the value is already in the UI state', () => {
- const [widget, helper, refine] = getInitializedWidget();
- refine({ from: 10, to: 20 });
-
- const uiStateBefore = {
- priceRanges: {
- price: '10:20',
- },
- };
-
- const uiStateAfter = widget.getWidgetState(uiStateBefore, {
- searchParameters: helper.state,
- helper,
- });
-
- expect(uiStateAfter).toBe(uiStateBefore);
- });
- });
-
- describe('getWidgetSearchParameters', () => {
- test('should return the same SP if no value is in the UI state', () => {
- const [widget, helper] = getInitializedWidget();
- // The user presses back (browser), the url contains no parameters
- const uiState = {};
- // The current search is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same empty parameters should yield the same object
- expect(searchParametersAfter).toBe(searchParametersBefore);
- });
-
- test('should return the same SP if the value from the UI state is the same', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // The user presses back (browser), the url contains min and max
- const uiState = {
- priceRanges: {
- price: '10:20',
- },
- };
- // The current search has the same parameters
- refine({ from: 10, to: 20 });
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same non empty parameters should yield the same object
- expect(searchParametersAfter).toBe(searchParametersBefore);
- });
-
- test('should return the same SP if the value from the UI state is the same (only min)', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // The user presses back (browser), the url contains min and max
- const uiState = {
- priceRanges: {
- price: '10:',
- },
- };
- // The current search has the same parameters
- refine({ from: 10 });
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same non empty parameters should yield the same object
- expect(searchParametersAfter).toBe(searchParametersBefore);
- });
-
- test('should return the same SP if the value from the UI state is the same (only max)', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // The user presses back (browser), the url contains min and max
- const uiState = {
- priceRanges: {
- price: ':20',
- },
- };
- // The current search has the same parameters
- refine({ to: 20 });
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same non empty parameters should yield the same object
- expect(searchParametersAfter).toBe(searchParametersBefore);
- });
-
- test('should keep the value that is not modified (min modified)', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // The user presses back (browser), the url contains min and max
- const uiState = {
- priceRanges: {
- price: '2:10',
- },
- };
- // The current search has the same parameters
- refine({ from: 4, to: 10 });
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same non empty parameters should yield the same object
- expect(
- searchParametersAfter.getNumericRefinement('price', '>=')[0]
- ).toBe(2);
- expect(
- searchParametersAfter.getNumericRefinement('price', '<=')[0]
- ).toBe(10);
- });
-
- test('should keep the value that is not modified (max modified)', () => {
- const [widget, helper, refine] = getInitializedWidget();
- // The user presses back (browser), the url contains min and max
- const uiState = {
- priceRanges: {
- price: '2:10',
- },
- };
- // The current search has the same parameters
- refine({ from: 2, to: 15 });
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the same non empty parameters should yield the same object
- expect(
- searchParametersAfter.getNumericRefinement('price', '>=')[0]
- ).toBe(2);
- expect(
- searchParametersAfter.getNumericRefinement('price', '<=')[0]
- ).toBe(10);
- });
-
- test('should add the refinements according to the UI state provided (min and max)', () => {
- const [widget, helper] = getInitializedWidget();
- // The user presses back (browser), the URL contains both min and max
- const uiState = {
- priceRanges: {
- price: '20:40',
- },
- };
- // The current state is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the new parameters should set two numeric refinements
- expect(searchParametersAfter).toMatchSnapshot();
- });
-
- test('should add the refinements according to the UI state provided (only max)', () => {
- const [widget, helper] = getInitializedWidget();
- // The user presses back (browser), the URL contains a max
- const uiState = {
- priceRanges: {
- price: ':50',
- },
- };
- // The current search is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the new parameters should set one refinement
- expect(searchParametersAfter).toMatchSnapshot();
- });
-
- test('should add the refinements according to the UI state provided (only min)', () => {
- const [widget, helper] = getInitializedWidget();
- // The user presses back (browser), the url contains a min
- const uiState = {
- priceRanges: {
- price: '10:',
- },
- };
- // the current search is empty
- const searchParametersBefore = SearchParameters.make(helper.state);
- const searchParametersAfter = widget.getWidgetSearchParameters(
- searchParametersBefore,
- { uiState }
- );
- // Applying the new parameter should set one refinement
- expect(searchParametersAfter).toMatchSnapshot();
- });
- });
- });
-});
diff --git a/src/connectors/price-ranges/__tests__/generate-ranges-test.js b/src/connectors/price-ranges/__tests__/generate-ranges-test.js
deleted file mode 100644
index 38c28132cb..0000000000
--- a/src/connectors/price-ranges/__tests__/generate-ranges-test.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import generateRanges from '../generate-ranges';
-
-describe('generateRanges()', () => {
- it('should generate ranges', () => {
- const stats = {
- min: 1.01,
- max: 4999.98,
- avg: 243.349,
- sum: 2433490.0,
- };
- const expected = [
- { to: 2 },
- { from: 2, to: 80 },
- { from: 80, to: 160 },
- { from: 160, to: 240 },
- { from: 240, to: 1820 },
- { from: 1820, to: 3400 },
- { from: 3400, to: 4980 },
- { from: 4980 },
- ];
- expect(generateRanges(stats)).toEqual(expected);
- });
-
- it('should generate small ranges', () => {
- const stats = { min: 20, max: 50, avg: 35, sum: 70 };
- const expected = [
- { to: 20 },
- { from: 20, to: 25 },
- { from: 25, to: 30 },
- { from: 30, to: 35 },
- { from: 35, to: 40 },
- { from: 40, to: 45 },
- { from: 45 },
- ];
- expect(generateRanges(stats)).toEqual(expected);
- });
-
- it('should not do an infinite loop', () => {
- const stats = { min: 99.99, max: 149.99, avg: 124.99, sum: 249.98 };
- const expected = [
- { to: 100 },
- { from: 100, to: 110 },
- { from: 110, to: 120 },
- { from: 120, to: 130 },
- { from: 130, to: 131 },
- { from: 131, to: 132 },
- { from: 132 },
- ];
- expect(generateRanges(stats)).toEqual(expected);
- });
-
- it('should not generate ranges', () => {
- const stats = { min: 20, max: 20, avg: 20, sum: 20 };
- expect(generateRanges(stats)).toEqual([]);
-
- const longerStats = { min: 6765, max: 6765, avg: 6765, sum: 6765 };
- expect(generateRanges(longerStats)).toEqual([]);
- });
-});
diff --git a/src/connectors/price-ranges/connectPriceRanges.js b/src/connectors/price-ranges/connectPriceRanges.js
deleted file mode 100644
index bd79d3b58b..0000000000
--- a/src/connectors/price-ranges/connectPriceRanges.js
+++ /dev/null
@@ -1,300 +0,0 @@
-import { checkRendering } from '../../lib/utils.js';
-import generateRanges from './generate-ranges.js';
-
-import isFinite from 'lodash/isFinite';
-
-const usage = `Usage:
-var customPriceRanges = connectPriceRanges(function render(params, isFirstRendering) {
- // params = {
- // items,
- // refine,
- // instantSearchInstance,
- // widgetParams,
- // }
-});
-search.addWidget(
- customPriceRanges({
- attributeName,
- })
-);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectPriceRanges.html
-`;
-
-/**
- * @typedef {Object} PriceRangesItem
- * @property {number} [from] Lower bound of the price range.
- * @property {number} [to] Higher bound of the price range.
- * @property {string} url The URL for a single item in the price range.
- */
-
-/**
- * @typedef {Object} CustomPriceRangesWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting.
- */
-
-/**
- * @typedef {Object} PriceRangesRenderingOptions
- * @property {PriceRangesItem[]} items The prices ranges to display.
- * @property {function(PriceRangesItem)} refine Selects or unselects a price range and trigger a search.
- * @property {Object} widgetParams All original `CustomPriceRangesWidgetOptions` forwarded to the `renderFn`.
- */
-
-/**
- * **PriceRanges** connector provides the logic to build a custom widget that will let
- * the user refine results based on price ranges.
- *
- * @type {Connector}
- * @param {function(PriceRangesRenderingOptions, boolean)} renderFn Rendering function for the custom **PriceRanges** widget.
- * @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomPriceRangesWidgetOptions)} Re-usable widget factory for a custom **PriceRanges** widget.
- * @example
- * function getLabel(item) {
- * var from = item.from;
- * var to = item.to;
- *
- * if (to === undefined) return '≥ $' + from;
- * if (from === undefined) return '≤ $' + to;
- * return '$' + from + ' - $' + to;
- * }
- *
- * // custom `renderFn` to render the custom PriceRanges widget
- * function renderFn(PriceRangesRenderingOptions, isFirstRendering) {
- * if (isFirstRendering) {
- * PriceRangesRenderingOptions.widgetParams.containerNode.html('');
- * }
- *
- * PriceRangesRenderingOptions.widgetParams.containerNode
- * .find('ul > li')
- * .each(function() { $(this).off('click'); });
- *
- * var list = PriceRangesRenderingOptions.items.map(function(item) {
- * return '' + getLabel(item) + ' ';
- * });
- *
- * PriceRangesRenderingOptions.widgetParams.containerNode
- * .find('ul')
- * .html(list);
- *
- * PriceRangesRenderingOptions.widgetParams.containerNode
- * .find('li')
- * .each(function(index) {
- * $(this).on('click', function(event) {
- * event.stopPropagation();
- * event.preventDefault();
- *
- * PriceRangesRenderingOptions.refine(
- * PriceRangesRenderingOptions.items[index]
- * );
- * });
- * });
- * }
- *
- * // connect `renderFn` to PriceRanges logic
- * var customPriceRanges = instantsearch.connectors.connectPriceRanges(renderFn);
- *
- * // mount widget on the page
- * search.addWidget(
- * customPriceRanges({
- * containerNode: $('#custom-price-ranges-container'),
- * attributeName: 'price',
- * })
- * );
- */
-export default function connectPriceRanges(renderFn, unmountFn) {
- checkRendering(renderFn, usage);
-
- return (widgetParams = {}) => {
- const { attributeName } = widgetParams;
-
- if (!attributeName) {
- throw new Error(usage);
- }
-
- return {
- getConfiguration() {
- return { facets: [attributeName] };
- },
-
- _generateRanges(results) {
- const stats = results.getFacetStats(attributeName);
- return generateRanges(stats);
- },
-
- _extractRefinedRange(helper) {
- const refinements = helper.getRefinements(attributeName);
- let from;
- let to;
-
- if (refinements.length === 0) {
- return [];
- }
-
- refinements.forEach(v => {
- if (v.operator.indexOf('>') !== -1) {
- from = Math.floor(v.value[0]);
- } else if (v.operator.indexOf('<') !== -1) {
- to = Math.ceil(v.value[0]);
- }
- });
- return [{ from, to, isRefined: true }];
- },
-
- _refine(helper, { from, to }) {
- const facetValues = this._extractRefinedRange(helper);
-
- helper.clearRefinements(attributeName);
- if (
- facetValues.length === 0 ||
- facetValues[0].from !== from ||
- facetValues[0].to !== to
- ) {
- if (typeof from !== 'undefined') {
- helper.addNumericRefinement(attributeName, '>=', Math.floor(from));
- }
- if (typeof to !== 'undefined') {
- helper.addNumericRefinement(attributeName, '<=', Math.ceil(to));
- }
- }
-
- helper.search();
- },
-
- init({ helper, instantSearchInstance }) {
- this.refine = opts => {
- this._refine(helper, opts);
- };
-
- renderFn(
- {
- instantSearchInstance,
- items: [],
- refine: this.refine,
- widgetParams,
- },
- true
- );
- },
-
- render({ results, helper, state, createURL, instantSearchInstance }) {
- let facetValues;
-
- if (results && results.hits && results.hits.length > 0) {
- facetValues = this._extractRefinedRange(helper);
-
- if (facetValues.length === 0) {
- facetValues = this._generateRanges(results);
- }
- } else {
- facetValues = [];
- }
-
- facetValues.map(facetValue => {
- let newState = state.clearRefinements(attributeName);
- if (!facetValue.isRefined) {
- if (facetValue.from !== undefined) {
- newState = newState.addNumericRefinement(
- attributeName,
- '>=',
- Math.floor(facetValue.from)
- );
- }
- if (facetValue.to !== undefined) {
- newState = newState.addNumericRefinement(
- attributeName,
- '<=',
- Math.ceil(facetValue.to)
- );
- }
- }
- facetValue.url = createURL(newState);
- return facetValue;
- });
-
- renderFn(
- {
- items: facetValues,
- refine: this.refine,
- widgetParams,
- instantSearchInstance,
- },
- false
- );
- },
-
- dispose({ state }) {
- unmountFn();
-
- const nextState = state
- .removeFacetRefinement(attributeName)
- .removeFacet(attributeName);
-
- return nextState;
- },
-
- getWidgetState(uiState, { searchParameters }) {
- const {
- '>=': min = '',
- '<=': max = '',
- } = searchParameters.getNumericRefinements(attributeName);
-
- if (
- (min === '' && max === '') ||
- (uiState &&
- uiState.priceRanges &&
- uiState.priceRanges[attributeName] === `${min}:${max}`)
- ) {
- return uiState;
- }
-
- return {
- ...uiState,
- priceRanges: {
- ...uiState.priceRanges,
- [attributeName]: `${min}:${max}`,
- },
- };
- },
-
- getWidgetSearchParameters(searchParameters, { uiState }) {
- const value =
- uiState && uiState.priceRanges && uiState.priceRanges[attributeName];
-
- if (!value || value.indexOf(':') === -1) {
- return searchParameters;
- }
-
- const {
- '>=': previousMin = [NaN],
- '<=': previousMax = [NaN],
- } = searchParameters.getNumericRefinements(attributeName);
- let clearedParams = searchParameters.clearRefinements(attributeName);
- const [lowerBound, upperBound] = value.split(':').map(parseFloat);
-
- if (
- previousMin.includes(lowerBound) &&
- previousMax.includes(upperBound)
- ) {
- return searchParameters;
- }
-
- if (isFinite(lowerBound)) {
- clearedParams = clearedParams.addNumericRefinement(
- attributeName,
- '>=',
- lowerBound
- );
- }
-
- if (isFinite(upperBound)) {
- clearedParams = clearedParams.addNumericRefinement(
- attributeName,
- '<=',
- upperBound
- );
- }
-
- return clearedParams;
- },
- };
- };
-}
diff --git a/src/connectors/price-ranges/generate-ranges.js b/src/connectors/price-ranges/generate-ranges.js
deleted file mode 100644
index 5d8dfce4ff..0000000000
--- a/src/connectors/price-ranges/generate-ranges.js
+++ /dev/null
@@ -1,83 +0,0 @@
-function round(v, precision) {
- let res = Math.round(v / precision) * precision;
- if (res < 1) {
- res = 1;
- }
- return res;
-}
-
-function generateRanges(stats) {
- // cannot compute any range
- if (stats.min === stats.max) {
- return [];
- }
-
- let precision;
- if (stats.avg < 100) {
- precision = 1;
- } else if (stats.avg < 1000) {
- precision = 10;
- } else {
- precision = 100;
- }
- const avg = round(Math.round(stats.avg), precision);
- const min = Math.ceil(stats.min);
- let max = round(Math.floor(stats.max), precision);
- while (max > stats.max) {
- max -= precision;
- }
-
- let next;
- let from;
- const facetValues = [];
- if (min !== max) {
- next = min;
-
- facetValues.push({
- to: next,
- });
-
- while (next < avg) {
- from = facetValues[facetValues.length - 1].to;
- next = round(from + (avg - min) / 3, precision);
- if (next <= from) {
- next = from + 1;
- }
- facetValues.push({
- from,
- to: next,
- });
- }
- while (next < max) {
- from = facetValues[facetValues.length - 1].to;
- next = round(from + (max - avg) / 3, precision);
- if (next <= from) {
- next = from + 1;
- }
- facetValues.push({
- from,
- to: next,
- });
- }
-
- if (facetValues.length === 1) {
- if (next !== avg) {
- facetValues.push({
- from: next,
- to: avg,
- });
- next = avg;
- }
- }
-
- if (facetValues.length === 1) {
- facetValues[0].from = stats.min;
- facetValues[0].to = stats.max;
- } else {
- delete facetValues[facetValues.length - 1].to;
- }
- }
- return facetValues;
-}
-
-export default generateRanges;
diff --git a/src/connectors/range-slider/connectRangeSlider.js b/src/connectors/range-slider/connectRangeSlider.js
deleted file mode 100644
index bb2f36107a..0000000000
--- a/src/connectors/range-slider/connectRangeSlider.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { deprecate } from '../../lib/utils';
-import connectRange from '../range/connectRange';
-
-export default deprecate(
- connectRange,
- `'connectRangeSlider' was replaced by 'connectRange'.
- Please see https://community.algolia.com/instantsearch.js/v2/connectors/connectRange.html`
-);
diff --git a/src/connectors/range/__tests__/connectRange-test.js b/src/connectors/range/__tests__/connectRange-test.js
index 28fb567b91..3c0f9ff5a4 100644
--- a/src/connectors/range/__tests__/connectRange-test.js
+++ b/src/connectors/range/__tests__/connectRange-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
@@ -11,21 +9,21 @@ describe('connectRange', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectRange(rendering);
- const attributeName = 'price';
+ const attribute = 'price';
const widget = makeWidget({
- attributeName,
+ attribute,
});
const config = widget.getConfiguration();
expect(config).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -36,16 +34,19 @@ describe('connectRange', () => {
{
// should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(1);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(true);
// should provide good values for the first rendering
- const { range, start, widgetParams } = rendering.lastCall.args[0];
+ const { range, start, widgetParams } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(range).toEqual({ min: 0, max: 0 });
expect(start).toEqual([-Infinity, Infinity]);
expect(widgetParams).toEqual({
- attributeName,
+ attribute,
precision: 2,
});
}
@@ -76,16 +77,19 @@ describe('connectRange', () => {
{
// Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(2);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(false);
// should provide good values for the first rendering
- const { range, start, widgetParams } = rendering.lastCall.args[0];
+ const { range, start, widgetParams } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(range).toEqual({ min: 10, max: 30 });
expect(start).toEqual([-Infinity, Infinity]);
expect(widgetParams).toEqual({
- attributeName,
+ attribute,
precision: 2,
});
}
@@ -94,28 +98,28 @@ describe('connectRange', () => {
it('Accepts some user bounds', () => {
const makeWidget = connectRange(() => {});
- const attributeName = 'price';
+ const attribute = 'price';
- expect(makeWidget({ attributeName, min: 0 }).getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
+ expect(makeWidget({ attribute, min: 0 }).getConfiguration()).toEqual({
+ disjunctiveFacets: [attribute],
numericRefinements: {
- [attributeName]: { '>=': [0] },
+ [attribute]: { '>=': [0] },
},
});
- expect(makeWidget({ attributeName, max: 100 }).getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
+ expect(makeWidget({ attribute, max: 100 }).getConfiguration()).toEqual({
+ disjunctiveFacets: [attribute],
numericRefinements: {
- [attributeName]: { '<=': [100] },
+ [attribute]: { '<=': [100] },
},
});
expect(
- makeWidget({ attributeName, min: 0, max: 100 }).getConfiguration()
+ makeWidget({ attribute, min: 0, max: 100 }).getConfiguration()
).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
numericRefinements: {
- [attributeName]: {
+ [attribute]: {
'>=': [0],
'<=': [100],
},
@@ -124,16 +128,16 @@ describe('connectRange', () => {
});
it('Provides a function to update the refinements at each step', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectRange(rendering);
- const attributeName = 'price';
+ const attribute = 'price';
const widget = makeWidget({
- attributeName,
+ attribute,
});
const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -146,12 +150,13 @@ describe('connectRange', () => {
// first rendering
expect(helper.getNumericRefinement('price', '>=')).toEqual(undefined);
expect(helper.getNumericRefinement('price', '<=')).toEqual(undefined);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine([10, 30]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
}
widget.render({
@@ -183,24 +188,25 @@ describe('connectRange', () => {
// Second rendering
expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine([23, 27]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([23]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([27]);
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
}
});
it('should add numeric refinement when refining min boundary without previous configuration', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectRange(rendering);
- const attributeName = 'price';
- const widget = makeWidget({ attributeName, min: 0, max: 500 });
+ const attribute = 'price';
+ const widget = makeWidget({ attribute, min: 0, max: 500 });
const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -214,13 +220,14 @@ describe('connectRange', () => {
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([500]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine([10, 30]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
refine([0, undefined]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
@@ -229,17 +236,17 @@ describe('connectRange', () => {
});
it('should add numeric refinement when refining min boundary with previous configuration', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectRange(rendering);
- const attributeName = 'price';
- const widget = makeWidget({ attributeName, min: 0, max: 500 });
+ const attribute = 'price';
+ const widget = makeWidget({ attribute, min: 0, max: 500 });
const configuration = widget.getConfiguration({
indexName: 'movie',
});
const helper = jsHelper({}, '', configuration);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -253,13 +260,14 @@ describe('connectRange', () => {
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([500]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine([10, 30]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([10]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([30]);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
refine([0, undefined]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
@@ -268,14 +276,14 @@ describe('connectRange', () => {
});
it('should refine on boundaries when no min/max defined', () => {
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectRange(rendering);
- const attributeName = 'price';
- const widget = makeWidget({ attributeName });
+ const attribute = 'price';
+ const widget = makeWidget({ attribute });
const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -288,34 +296,35 @@ describe('connectRange', () => {
expect(helper.getNumericRefinement('price', '>=')).toEqual(undefined);
expect(helper.getNumericRefinement('price', '<=')).toEqual(undefined);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine } = renderOptions;
refine([undefined, 100]);
expect(helper.getNumericRefinement('price', '>=')).toEqual(undefined);
expect(helper.getNumericRefinement('price', '<=')).toEqual([100]);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
refine([0, undefined]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
expect(helper.getNumericRefinement('price', '<=')).toEqual(undefined);
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
refine([0, 100]);
expect(helper.getNumericRefinement('price', '>=')).toEqual([0]);
expect(helper.getNumericRefinement('price', '<=')).toEqual([100]);
- expect(helper.search.callCount).toBe(3);
+ expect(helper.search).toHaveBeenCalledTimes(3);
}
});
describe('getConfiguration', () => {
- const attributeName = 'price';
+ const attribute = 'price';
const rendering = () => {};
it('expect to return default configuration', () => {
const currentConfiguration = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
});
const expectation = { disjunctiveFacets: ['price'] };
@@ -334,7 +343,7 @@ describe('connectRange', () => {
};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
max: 500,
});
@@ -347,7 +356,7 @@ describe('connectRange', () => {
it('expect to return default configuration if the given min bound are greater than max bound', () => {
const currentConfiguration = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 1000,
max: 500,
});
@@ -361,7 +370,7 @@ describe('connectRange', () => {
it('expect to return configuration with min numeric refinement', () => {
const currentConfiguration = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 10,
});
@@ -382,7 +391,7 @@ describe('connectRange', () => {
it('expect to return configuration with max numeric refinement', () => {
const currentConfiguration = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
max: 10,
});
@@ -403,7 +412,7 @@ describe('connectRange', () => {
it('expect to return configuration with both numeric refinements', () => {
const currentConfiguration = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 10,
max: 500,
});
@@ -425,13 +434,13 @@ describe('connectRange', () => {
});
describe('_getCurrentRange', () => {
- const attributeName = 'price';
+ const attribute = 'price';
const rendering = () => {};
it('expect to return default range', () => {
const stats = {};
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
});
const expectation = { min: 0, max: 0 };
@@ -443,7 +452,7 @@ describe('connectRange', () => {
it('expect to return range from bounds', () => {
const stats = { min: 10, max: 500 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 20,
max: 250,
});
@@ -457,7 +466,7 @@ describe('connectRange', () => {
it('expect to return range from stats', () => {
const stats = { min: 10, max: 500 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
});
const expectation = { min: 10, max: 500 };
@@ -469,7 +478,7 @@ describe('connectRange', () => {
it('expect to return rounded range values when precision is 0', () => {
const stats = { min: 1.79, max: 499.99 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
precision: 0,
});
@@ -482,7 +491,7 @@ describe('connectRange', () => {
it('expect to return rounded range values when precision is 1', () => {
const stats = { min: 1.12345, max: 499.56789 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
precision: 1,
});
@@ -495,7 +504,7 @@ describe('connectRange', () => {
it('expect to return rounded range values when precision is 2', () => {
const stats = { min: 1.12345, max: 499.56789 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
precision: 2,
});
@@ -508,7 +517,7 @@ describe('connectRange', () => {
it('expect to return rounded range values when precision is 3', () => {
const stats = { min: 1.12345, max: 499.56789 };
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
precision: 3,
});
@@ -520,12 +529,12 @@ describe('connectRange', () => {
});
describe('_getCurrentRefinement', () => {
- const attributeName = 'price';
+ const attribute = 'price';
const rendering = () => {};
const createHelper = () => jsHelper({});
it('expect to return default refinement', () => {
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
const helper = createHelper();
const expectation = [-Infinity, Infinity];
@@ -535,11 +544,11 @@ describe('connectRange', () => {
});
it('expect to return refinement from helper', () => {
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
const helper = createHelper();
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 100);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 100);
const expectation = [10, 100];
const actual = widget._getCurrentRefinement(helper);
@@ -548,11 +557,11 @@ describe('connectRange', () => {
});
it('expect to return float refinement values', () => {
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
const helper = createHelper();
- helper.addNumericRefinement(attributeName, '>=', 10.9);
- helper.addNumericRefinement(attributeName, '<=', 99.1);
+ helper.addNumericRefinement(attribute, '>=', 10.9);
+ helper.addNumericRefinement(attribute, '<=', 99.1);
const expectation = [10.9, 99.1];
const actual = widget._getCurrentRefinement(helper);
@@ -562,7 +571,7 @@ describe('connectRange', () => {
});
describe('_refine', () => {
- const attributeName = 'price';
+ const attribute = 'price';
const rendering = () => {};
const createHelper = () => {
const helper = jsHelper({});
@@ -580,14 +589,14 @@ describe('connectRange', () => {
const values = [10, 490];
const helper = createHelper();
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
});
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -595,13 +604,13 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [10, 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -609,12 +618,12 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = ['10', '490'];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -623,12 +632,12 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = ['10.50', '490.50'];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10.5]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490.5]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10.5]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490.5]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -638,14 +647,14 @@ describe('connectRange', () => {
const values = [10, 490];
const helper = createHelper();
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 10,
});
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -655,14 +664,14 @@ describe('connectRange', () => {
const values = [10, 490];
const helper = createHelper();
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
max: 490,
});
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -671,18 +680,16 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [undefined, 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -690,18 +697,16 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [10, undefined];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -709,18 +714,16 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = ['', 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -728,18 +731,16 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [10, ''];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -747,17 +748,15 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [0, 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
+ helper.addNumericRefinement(attribute, '>=', 10);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -765,17 +764,15 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [10, 500];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
- expect(helper.clearRefinements).toHaveBeenCalledWith(attributeName);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
+ expect(helper.clearRefinements).toHaveBeenCalledWith(attribute);
expect(helper.search).toHaveBeenCalled();
});
@@ -784,16 +781,16 @@ describe('connectRange', () => {
const values = [undefined, 490];
const helper = createHelper();
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
min: 10,
});
- helper.addNumericRefinement(attributeName, '>=', 20);
+ helper.addNumericRefinement(attribute, '>=', 20);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -803,16 +800,16 @@ describe('connectRange', () => {
const values = [10, undefined];
const helper = createHelper();
const widget = connectRange(rendering)({
- attributeName,
+ attribute,
max: 250,
});
- helper.addNumericRefinement(attributeName, '>=', 240);
+ helper.addNumericRefinement(attribute, '>=', 240);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([250]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([250]);
expect(helper.clearRefinements).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
});
@@ -821,16 +818,12 @@ describe('connectRange', () => {
const range = { min: 10, max: 500 };
const values = [0, 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
expect(helper.clearRefinements).not.toHaveBeenCalled();
expect(helper.search).not.toHaveBeenCalled();
});
@@ -839,16 +832,12 @@ describe('connectRange', () => {
const range = { min: 0, max: 490 };
const values = [10, 500];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
expect(helper.clearRefinements).not.toHaveBeenCalled();
expect(helper.search).not.toHaveBeenCalled();
});
@@ -857,16 +846,12 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [undefined, undefined];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
expect(helper.clearRefinements).not.toHaveBeenCalled();
expect(helper.search).not.toHaveBeenCalled();
});
@@ -875,15 +860,15 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = [10, 490];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 490);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 490);
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual([10]);
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual([490]);
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual([10]);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual([490]);
expect(helper.clearRefinements).not.toHaveBeenCalled();
expect(helper.search).not.toHaveBeenCalled();
});
@@ -892,16 +877,12 @@ describe('connectRange', () => {
const range = { min: 0, max: 500 };
const values = ['ADASA', 'FFDSFQS'];
const helper = createHelper();
- const widget = connectRange(rendering)({ attributeName });
+ const widget = connectRange(rendering)({ attribute });
widget._refine(helper, range)(values);
- expect(helper.getNumericRefinement(attributeName, '>=')).toEqual(
- undefined
- );
- expect(helper.getNumericRefinement(attributeName, '<=')).toEqual(
- undefined
- );
+ expect(helper.getNumericRefinement(attribute, '>=')).toEqual(undefined);
+ expect(helper.getNumericRefinement(attribute, '<=')).toEqual(undefined);
expect(helper.clearRefinements).not.toHaveBeenCalled();
expect(helper.search).not.toHaveBeenCalled();
});
@@ -912,7 +893,7 @@ describe('connectRange', () => {
const rendering = jest.fn();
const makeWidget = connectRange(rendering);
const widget = makeWidget({
- attributeName: 'price',
+ attribute: 'price',
});
const config = widget.getConfiguration({}, {});
diff --git a/src/connectors/range/connectRange.js b/src/connectors/range/connectRange.js
index e44219ffc5..4b2b67ad38 100644
--- a/src/connectors/range/connectRange.js
+++ b/src/connectors/range/connectRange.js
@@ -16,7 +16,7 @@ var customRange = connectRange(function render(params, isFirstRendering) {
});
search.addWidget(
customRange({
- attributeName,
+ attribute,
[ min ],
[ max ],
[ precision = 2 ],
@@ -27,7 +27,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomRangeWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting.
+ * @property {string} attribute Name of the attribute for faceting.
* @property {number} [min = undefined] Minimal range value, default to automatically computed from the result set.
* @property {number} [max = undefined] Maximal range value, default to automatically computed from the result set.
* @property {number} [precision = 2] Number of digits after decimal point to use.
@@ -61,13 +61,13 @@ export default function connectRange(renderFn, unmountFn) {
return (widgetParams = {}) => {
const {
- attributeName,
+ attribute,
min: minBound,
max: maxBound,
precision = 2,
} = widgetParams;
- if (!attributeName) {
+ if (!attribute) {
throw new Error(usage);
}
@@ -110,11 +110,9 @@ export default function connectRange(renderFn, unmountFn) {
},
_getCurrentRefinement(helper) {
- const [minValue] =
- helper.getNumericRefinement(attributeName, '>=') || [];
+ const [minValue] = helper.getNumericRefinement(attribute, '>=') || [];
- const [maxValue] =
- helper.getNumericRefinement(attributeName, '<=') || [];
+ const [maxValue] = helper.getNumericRefinement(attribute, '<=') || [];
const min = _isFinite(minValue) ? minValue : -Infinity;
const max = _isFinite(maxValue) ? maxValue : Infinity;
@@ -127,8 +125,8 @@ export default function connectRange(renderFn, unmountFn) {
return ([nextMin, nextMax] = []) => {
const { min: currentRangeMin, max: currentRangeMax } = currentRange;
- const [min] = helper.getNumericRefinement(attributeName, '>=') || [];
- const [max] = helper.getNumericRefinement(attributeName, '<=') || [];
+ const [min] = helper.getNumericRefinement(attribute, '>=') || [];
+ const [max] = helper.getNumericRefinement(attribute, '<=') || [];
const isResetMin = nextMin === undefined || nextMin === '';
const isResetMax = nextMax === undefined || nextMax === '';
@@ -178,11 +176,11 @@ export default function connectRange(renderFn, unmountFn) {
const hasMaxChange = max !== newNextMax;
if ((hasMinChange || hasMaxChange) && (isMinValid && isMaxValid)) {
- helper.clearRefinements(attributeName);
+ helper.clearRefinements(attribute);
if (isValidNewNextMin) {
helper.addNumericRefinement(
- attributeName,
+ attribute,
'>=',
formatToNumber(newNextMin)
);
@@ -190,7 +188,7 @@ export default function connectRange(renderFn, unmountFn) {
if (isValidNewNextMax) {
helper.addNumericRefinement(
- attributeName,
+ attribute,
'<=',
formatToNumber(newNextMax)
);
@@ -203,7 +201,7 @@ export default function connectRange(renderFn, unmountFn) {
getConfiguration(currentConfiguration) {
const configuration = {
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
};
const isBoundsDefined = hasMinBound || hasMaxBound;
@@ -211,7 +209,7 @@ export default function connectRange(renderFn, unmountFn) {
const boundsAlreadyDefined =
currentConfiguration &&
currentConfiguration.numericRefinements &&
- currentConfiguration.numericRefinements[attributeName] !== undefined;
+ currentConfiguration.numericRefinements[attribute] !== undefined;
const isMinBoundValid = _isFinite(minBound);
const isMaxBoundValid = _isFinite(maxBound);
@@ -221,14 +219,14 @@ export default function connectRange(renderFn, unmountFn) {
: isMinBoundValid || isMaxBoundValid;
if (isBoundsDefined && !boundsAlreadyDefined && isAbleToRefine) {
- configuration.numericRefinements = { [attributeName]: {} };
+ configuration.numericRefinements = { [attribute]: {} };
if (hasMinBound) {
- configuration.numericRefinements[attributeName]['>='] = [minBound];
+ configuration.numericRefinements[attribute]['>='] = [minBound];
}
if (hasMaxBound) {
- configuration.numericRefinements[attributeName]['<='] = [maxBound];
+ configuration.numericRefinements[attribute]['<='] = [maxBound];
}
}
@@ -261,7 +259,7 @@ export default function connectRange(renderFn, unmountFn) {
render({ results, helper, instantSearchInstance }) {
const facetsFromResults = results.disjunctiveFacets || [];
- const facet = find(facetsFromResults, { name: attributeName });
+ const facet = find(facetsFromResults, { name: attribute });
const stats = (facet && facet.stats) || {};
const currentRange = this._getCurrentRange(stats);
@@ -287,8 +285,8 @@ export default function connectRange(renderFn, unmountFn) {
unmountFn();
const nextState = state
- .removeNumericRefinement(attributeName)
- .removeDisjunctiveFacet(attributeName);
+ .removeNumericRefinement(attribute)
+ .removeDisjunctiveFacet(attribute);
return nextState;
},
@@ -297,13 +295,13 @@ export default function connectRange(renderFn, unmountFn) {
const {
'>=': min = '',
'<=': max = '',
- } = searchParameters.getNumericRefinements(attributeName);
+ } = searchParameters.getNumericRefinements(attribute);
if (
(min === '' && max === '') ||
(uiState &&
uiState.range &&
- uiState.range[attributeName] === `${min}:${max}`)
+ uiState.range[attribute] === `${min}:${max}`)
) {
return uiState;
}
@@ -312,13 +310,13 @@ export default function connectRange(renderFn, unmountFn) {
...uiState,
range: {
...uiState.range,
- [attributeName]: `${min}:${max}`,
+ [attribute]: `${min}:${max}`,
},
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
- const value = uiState && uiState.range && uiState.range[attributeName];
+ const value = uiState && uiState.range && uiState.range[attribute];
if (!value || value.indexOf(':') === -1) {
return searchParameters;
@@ -327,8 +325,8 @@ export default function connectRange(renderFn, unmountFn) {
const {
'>=': previousMin = [NaN],
'<=': previousMax = [NaN],
- } = searchParameters.getNumericRefinements(attributeName);
- let clearedParams = searchParameters.clearRefinements(attributeName);
+ } = searchParameters.getNumericRefinements(attribute);
+ let clearedParams = searchParameters.clearRefinements(attribute);
const [lowerBound, upperBound] = value.split(':').map(parseFloat);
if (
@@ -340,7 +338,7 @@ export default function connectRange(renderFn, unmountFn) {
if (_isFinite(lowerBound)) {
clearedParams = clearedParams.addNumericRefinement(
- attributeName,
+ attribute,
'>=',
lowerBound
);
@@ -348,7 +346,7 @@ export default function connectRange(renderFn, unmountFn) {
if (_isFinite(upperBound)) {
clearedParams = clearedParams.addNumericRefinement(
- attributeName,
+ attribute,
'<=',
upperBound
);
diff --git a/src/connectors/star-rating/__tests__/__snapshots__/connectStarRating-test.js.snap b/src/connectors/rating-menu/__tests__/__snapshots__/connectRatingMenu-test.js.snap
similarity index 92%
rename from src/connectors/star-rating/__tests__/__snapshots__/connectStarRating-test.js.snap
rename to src/connectors/rating-menu/__tests__/__snapshots__/connectRatingMenu-test.js.snap
index 7277523412..e30ae114e7 100644
--- a/src/connectors/star-rating/__tests__/__snapshots__/connectStarRating-test.js.snap
+++ b/src/connectors/rating-menu/__tests__/__snapshots__/connectRatingMenu-test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`connectStarRating routing getWidgetSearchParameters should add the refinements according to the UI state provided 1`] = `
+exports[`connectRatingMenu routing getWidgetSearchParameters should add the refinements according to the UI state provided 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -66,9 +66,9 @@ SearchParameters {
}
`;
-exports[`connectStarRating routing getWidgetState should add an entry equal to the refinement 1`] = `
+exports[`connectRatingMenu routing getWidgetState should add an entry equal to the refinement 1`] = `
Object {
- "starRating": Object {
+ "ratingMenu": Object {
"grade": 3,
},
}
diff --git a/src/connectors/star-rating/__tests__/connectStarRating-test.js b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js
similarity index 79%
rename from src/connectors/star-rating/__tests__/connectStarRating-test.js
rename to src/connectors/rating-menu/__tests__/connectRatingMenu-test.js
index 8a3f3c6df1..14d7af2c80 100644
--- a/src/connectors/star-rating/__tests__/connectStarRating-test.js
+++ b/src/connectors/rating-menu/__tests__/connectRatingMenu-test.js
@@ -1,31 +1,28 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
} from 'algoliasearch-helper';
+import connectRatingMenu from '../connectRatingMenu.js';
-import connectStarRating from '../connectStarRating.js';
-
-describe('connectStarRating', () => {
+describe('connectRatingMenu', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
- const makeWidget = connectStarRating(rendering);
+ const rendering = jest.fn();
+ const makeWidget = connectRatingMenu(rendering);
- const attributeName = 'grade';
+ const attribute = 'grade';
const widget = makeWidget({
- attributeName,
+ attribute,
});
const config = widget.getConfiguration({});
expect(config).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -36,21 +33,24 @@ describe('connectStarRating', () => {
{
// should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(1);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(true);
// should provide good values for the first rendering
- const { items, widgetParams } = rendering.lastCall.args[0];
+ const { items, widgetParams } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(items).toEqual([]);
- expect(widgetParams).toEqual({ attributeName });
+ expect(widgetParams).toEqual({ attribute });
}
widget.render({
results: new SearchResults(helper.state, [
{
facets: {
- [attributeName]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
+ [attribute]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
},
},
{},
@@ -62,12 +62,15 @@ describe('connectStarRating', () => {
{
// Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(2);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(false);
// should provide good values after the first search
- const { items } = rendering.lastCall.args[0];
+ const { items } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(items).toEqual([
{
count: 1000,
@@ -102,18 +105,18 @@ describe('connectStarRating', () => {
});
it('Provides a function to update the index at each step', () => {
- const rendering = sinon.stub();
- const makeWidget = connectStarRating(rendering);
+ const rendering = jest.fn();
+ const makeWidget = connectRatingMenu(rendering);
- const attributeName = 'grade';
+ const attribute = 'grade';
const widget = makeWidget({
- attributeName,
+ attribute,
});
const config = widget.getConfiguration({});
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -124,29 +127,30 @@ describe('connectStarRating', () => {
{
// first rendering
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, items } = renderOptions;
expect(items).toEqual([]);
- expect(helper.getRefinements(attributeName)).toEqual([]);
+ expect(helper.getRefinements(attribute)).toEqual([]);
refine('3');
- expect(helper.getRefinements(attributeName)).toEqual([
+ expect(helper.getRefinements(attribute)).toEqual([
{ type: 'disjunctive', value: '3' },
{ type: 'disjunctive', value: '4' },
{ type: 'disjunctive', value: '5' },
]);
- expect(helper.search.callCount).toBe(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
}
widget.render({
results: new SearchResults(helper.state, [
{
facets: {
- [attributeName]: { 3: 50, 4: 900, 5: 100 },
+ [attribute]: { 3: 50, 4: 900, 5: 100 },
},
},
{
facets: {
- [attributeName]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
+ [attribute]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
},
},
]),
@@ -157,7 +161,8 @@ describe('connectStarRating', () => {
{
// Second rendering
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, items } = renderOptions;
expect(items).toEqual([
{
@@ -189,28 +194,28 @@ describe('connectStarRating', () => {
stars: [true, false, false, false, false],
},
]);
- expect(helper.getRefinements(attributeName)).toEqual([
+ expect(helper.getRefinements(attribute)).toEqual([
{ type: 'disjunctive', value: '3' },
{ type: 'disjunctive', value: '4' },
{ type: 'disjunctive', value: '5' },
]);
refine('4');
- expect(helper.getRefinements(attributeName)).toEqual([
+ expect(helper.getRefinements(attribute)).toEqual([
{ type: 'disjunctive', value: '4' },
{ type: 'disjunctive', value: '5' },
]);
- expect(helper.search.callCount).toBe(2);
+ expect(helper.search).toHaveBeenCalledTimes(2);
}
});
describe('routing', () => {
const getInitializedWidget = (config = {}) => {
const rendering = jest.fn();
- const makeWidget = connectStarRating(rendering);
+ const makeWidget = connectRatingMenu(rendering);
- const attributeName = 'grade';
+ const attribute = 'grade';
const widget = makeWidget({
- attributeName,
+ attribute,
...config,
});
@@ -285,7 +290,7 @@ describe('connectStarRating', () => {
test('should add the refinements according to the UI state provided', () => {
const [widget, helper] = getInitializedWidget();
const uiState = {
- starRating: {
+ ratingMenu: {
grade: '2',
},
};
diff --git a/src/connectors/star-rating/connectStarRating.js b/src/connectors/rating-menu/connectRatingMenu.js
similarity index 84%
rename from src/connectors/star-rating/connectStarRating.js
rename to src/connectors/rating-menu/connectRatingMenu.js
index e9819be905..985aa1de06 100644
--- a/src/connectors/star-rating/connectStarRating.js
+++ b/src/connectors/rating-menu/connectRatingMenu.js
@@ -1,7 +1,7 @@
import { checkRendering } from '../../lib/utils.js';
const usage = `Usage:
-var customStarRating = connectStarRating(function render(params, isFirstRendering) {
+var customStarRating = connectRatingMenu(function render(params, isFirstRendering) {
// params = {
// items,
// createURL,
@@ -13,11 +13,11 @@ var customStarRating = connectStarRating(function render(params, isFirstRenderin
});
search.addWidget(
customStarRatingI({
- attributeName,
+ attribute,
[ max=5 ],
})
);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectStarRating.html
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectRatingMenu.html
`;
/**
@@ -31,7 +31,7 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomStarRatingWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
+ * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping").
* @property {number} [max = 5] The maximum rating value.
*/
@@ -93,36 +93,36 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* }
*
* // connect `renderFn` to StarRating logic
- * var customStarRating = instantsearch.connectors.connectStarRating(renderFn);
+ * var customStarRating = instantsearch.connectors.connectRatingMenu(renderFn);
*
* // mount widget on the page
* search.addWidget(
* customStarRating({
- * containerNode: $('#custom-star-rating-container'),
- * attributeName: 'rating',
+ * containerNode: $('#custom-rating-menu-container'),
+ * attribute: 'rating',
* max: 5,
* })
* );
*/
-export default function connectStarRating(renderFn, unmountFn) {
+export default function connectRatingMenu(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { attributeName, max = 5 } = widgetParams;
+ const { attribute, max = 5 } = widgetParams;
- if (!attributeName) {
+ if (!attribute) {
throw new Error(usage);
}
return {
getConfiguration() {
- return { disjunctiveFacets: [attributeName] };
+ return { disjunctiveFacets: [attribute] };
},
init({ helper, createURL, instantSearchInstance }) {
this._toggleRefinement = this._toggleRefinement.bind(this, helper);
this._createURL = state => facetValue =>
- createURL(state.toggleRefinement(attributeName, facetValue));
+ createURL(state.toggleRefinement(attribute, facetValue));
renderFn(
{
@@ -143,7 +143,7 @@ export default function connectStarRating(renderFn, unmountFn) {
for (let v = max; v >= 0; --v) {
allValues[v] = 0;
}
- results.getFacetValues(attributeName).forEach(facet => {
+ results.getFacetValues(attribute).forEach(facet => {
const val = Math.round(facet.name);
if (!val || val > max) {
return;
@@ -190,8 +190,8 @@ export default function connectStarRating(renderFn, unmountFn) {
unmountFn();
const nextState = state
- .removeDisjunctiveFacetRefinement(attributeName)
- .removeDisjunctiveFacet(attributeName);
+ .removeDisjunctiveFacetRefinement(attribute)
+ .removeDisjunctiveFacet(attribute);
return nextState;
},
@@ -201,34 +201,32 @@ export default function connectStarRating(renderFn, unmountFn) {
if (
refinedStar === undefined ||
(uiState &&
- uiState.starRating &&
- uiState.starRating[attributeName] === refinedStar)
+ uiState.ratingMenu &&
+ uiState.ratingMenu[attribute] === refinedStar)
)
return uiState;
return {
...uiState,
- starRating: {
- ...uiState.starRating,
- [attributeName]: refinedStar,
+ ratingMenu: {
+ ...uiState.ratingMenu,
+ [attribute]: refinedStar,
},
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
const starRatingFromURL =
- uiState.starRating && uiState.starRating[attributeName];
+ uiState.ratingMenu && uiState.ratingMenu[attribute];
const refinedStar = this._getRefinedStar(searchParameters);
if (starRatingFromURL === refinedStar) return searchParameters;
- let clearedSearchParam = searchParameters.clearRefinements(
- attributeName
- );
+ let clearedSearchParam = searchParameters.clearRefinements(attribute);
if (starRatingFromURL !== undefined) {
for (let val = Number(starRatingFromURL); val <= max; ++val) {
clearedSearchParam = clearedSearchParam.addDisjunctiveFacetRefinement(
- attributeName,
+ attribute,
val
);
}
@@ -240,10 +238,10 @@ export default function connectStarRating(renderFn, unmountFn) {
_toggleRefinement(helper, facetValue) {
const isRefined =
this._getRefinedStar(helper.state) === Number(facetValue);
- helper.clearRefinements(attributeName);
+ helper.clearRefinements(attribute);
if (!isRefined) {
for (let val = Number(facetValue); val <= max; ++val) {
- helper.addDisjunctiveFacetRefinement(attributeName, val);
+ helper.addDisjunctiveFacetRefinement(attribute, val);
}
}
helper.search();
@@ -252,7 +250,7 @@ export default function connectStarRating(renderFn, unmountFn) {
_getRefinedStar(searchParameters) {
let refinedStar = undefined;
const refinements = searchParameters.getDisjunctiveRefinements(
- attributeName
+ attribute
);
refinements.forEach(r => {
if (!refinedStar || Number(r) < refinedStar) {
diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
index 11bc496276..915de6cc58 100644
--- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
+++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.js
@@ -2,8 +2,7 @@ import jsHelper, {
SearchResults,
SearchParameters,
} from 'algoliasearch-helper';
-import { tagConfig } from '../../../lib/escape-highlight.js';
-
+import { TAG_PLACEHOLDER } from '../../../lib/escape-highlight.js';
import connectRefinementList from '../connectRefinementList.js';
describe('connectRefinementList', () => {
@@ -32,17 +31,39 @@ describe('connectRefinementList', () => {
expect(() =>
connectRefinementList(() => {})({
- attributeName: 'company',
+ attribute: 'company',
operator: 'YUP',
})
).toThrow(/Usage:/);
+
+ expect(() =>
+ connectRefinementList(() => {})({
+ attribute: 'company',
+ limit: 10,
+ showMore: true,
+ showMoreLimit: 10,
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\`showMoreLimit\` should be greater than \`limit\`."`
+ );
+
+ expect(() =>
+ connectRefinementList(() => {})({
+ attribute: 'company',
+ limit: 10,
+ showMore: true,
+ showMoreLimit: 5,
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"\`showMoreLimit\` should be greater than \`limit\`."`
+ );
});
describe('options configuring the helper', () => {
- it('`attributeName`', () => {
+ it('`attribute`', () => {
const { makeWidget } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
});
expect(widget.getConfiguration()).toEqual({
@@ -54,7 +75,7 @@ describe('connectRefinementList', () => {
it('`limit`', () => {
const { makeWidget } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 20,
});
@@ -72,10 +93,128 @@ describe('connectRefinementList', () => {
);
});
+ it('`showMoreLimit`', () => {
+ const { rendering, makeWidget } = createWidgetFactory();
+ const widget = makeWidget({
+ attribute: 'myFacet',
+ limit: 20,
+ showMore: true,
+ showMoreLimit: 30,
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ c3: 880,
+ c4: 47,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ c3: 880,
+ c4: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ secondRenderingOptions.toggleShowMore();
+
+ expect(widget.getConfiguration()).toEqual({
+ disjunctiveFacets: ['myFacet'],
+ maxValuesPerFacet: 30,
+ });
+
+ expect(widget.getConfiguration({ maxValuesPerFacet: 100 })).toEqual({
+ disjunctiveFacets: ['myFacet'],
+ maxValuesPerFacet: 100,
+ });
+ });
+
+ it('`showMoreLimit` without `showMore` does not set anything', () => {
+ const { rendering, makeWidget } = createWidgetFactory();
+ const widget = makeWidget({
+ attribute: 'myFacet',
+ limit: 20,
+ showMoreLimit: 30,
+ });
+
+ const helper = jsHelper({}, '', widget.getConfiguration({}));
+ helper.search = jest.fn();
+
+ widget.init({
+ helper,
+ state: helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ });
+
+ widget.render({
+ results: new SearchResults(helper.state, [
+ {
+ hits: [],
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ c3: 880,
+ c4: 47,
+ },
+ },
+ },
+ {
+ facets: {
+ category: {
+ c1: 880,
+ c2: 47,
+ c3: 880,
+ c4: 47,
+ },
+ },
+ },
+ ]),
+ state: helper.state,
+ helper,
+ createURL: () => '#',
+ });
+
+ const secondRenderingOptions = rendering.mock.calls[1][0];
+ secondRenderingOptions.toggleShowMore();
+
+ expect(widget.getConfiguration()).toEqual({
+ disjunctiveFacets: ['myFacet'],
+ maxValuesPerFacet: 20,
+ });
+ });
+
it('`operator="and"`', () => {
const { makeWidget } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
operator: 'and',
});
@@ -91,7 +230,7 @@ describe('connectRefinementList', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
const widget = makeWidget({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
});
@@ -122,7 +261,7 @@ describe('connectRefinementList', () => {
const firstRenderingOptions = rendering.mock.calls[0][0];
expect(firstRenderingOptions.canRefine).toBe(false);
expect(firstRenderingOptions.widgetParams).toEqual({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
});
@@ -140,7 +279,7 @@ describe('connectRefinementList', () => {
const secondRenderingOptions = rendering.mock.calls[1][0];
expect(secondRenderingOptions.canRefine).toBe(false);
expect(secondRenderingOptions.widgetParams).toEqual({
- attributeName: 'myFacet',
+ attribute: 'myFacet',
limit: 9,
});
});
@@ -148,7 +287,7 @@ describe('connectRefinementList', () => {
it('transforms items if requested', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
transformItems: items =>
items.map(item => ({
...item,
@@ -211,7 +350,7 @@ describe('connectRefinementList', () => {
it('Provide a function to clear the refinements at each step', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
@@ -251,8 +390,9 @@ describe('connectRefinementList', () => {
it('If there are too few items then canToggleShowMore is false', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 3,
+ showMore: true,
showMoreLimit: 10,
});
@@ -298,7 +438,7 @@ describe('connectRefinementList', () => {
it('If there are no showMoreLimit specified, canToggleShowMore is false', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 1,
});
@@ -348,8 +488,9 @@ describe('connectRefinementList', () => {
it('If there are same amount of items then canToggleShowMore is false', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
+ showMore: true,
showMoreLimit: 10,
});
@@ -399,8 +540,9 @@ describe('connectRefinementList', () => {
it('If there are enough items then canToggleShowMore is true', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 1,
+ showMore: true,
showMoreLimit: 10,
});
@@ -453,8 +595,9 @@ describe('connectRefinementList', () => {
it('Show more should toggle between two limits', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 1,
+ showMore: true,
showMoreLimit: 3,
});
@@ -546,7 +689,7 @@ describe('connectRefinementList', () => {
it('hasExhaustiveItems indicates if the items provided are exhaustive - without other widgets making the maxValuesPerFacet bigger', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
});
@@ -623,7 +766,7 @@ describe('connectRefinementList', () => {
it('hasExhaustiveItems indicates if the items provided are exhaustive - with an other widgets making the maxValuesPerFacet bigger', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
});
@@ -704,8 +847,9 @@ describe('connectRefinementList', () => {
it('can search in facet values', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
+ escapeFacetValues: false,
});
const helper = jsHelper({}, '', widget.getConfiguration({}));
@@ -716,12 +860,12 @@ describe('connectRefinementList', () => {
facetHits: [
{
count: 33,
- highlighted: 'Salvador Da li',
+ highlighted: 'Salvador Da li',
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: 'Da vidoff',
+ highlighted: 'Da vidoff',
value: 'Davidoff',
},
],
@@ -777,8 +921,8 @@ describe('connectRefinementList', () => {
expect(sffvFacet).toBe('category');
expect(maxNbItems).toBe(2);
expect(paramOverride).toEqual({
- highlightPreTag: undefined,
- highlightPostTag: undefined,
+ highlightPreTag: '',
+ highlightPostTag: ' ',
});
return Promise.resolve().then(() => {
@@ -786,13 +930,13 @@ describe('connectRefinementList', () => {
expect(rendering.mock.calls[2][0].items).toEqual([
{
count: 33,
- highlighted: 'Salvador Da li',
+ highlighted: 'Salvador Da li',
label: 'Salvador Dali',
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: 'Da vidoff',
+ highlighted: 'Da vidoff',
label: 'Davidoff',
value: 'Davidoff',
},
@@ -803,8 +947,9 @@ describe('connectRefinementList', () => {
it('can search in facet values with transformed items', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
+ escapeFacetValues: false,
transformItems: items =>
items.map(item => ({
...item,
@@ -883,8 +1028,8 @@ describe('connectRefinementList', () => {
expect(sffvFacet).toBe('category');
expect(maxNbItems).toBe(2);
expect(paramOverride).toEqual({
- highlightPreTag: undefined,
- highlightPostTag: undefined,
+ highlightPreTag: '',
+ highlightPostTag: ' ',
});
return Promise.resolve().then(() => {
@@ -907,14 +1052,15 @@ describe('connectRefinementList', () => {
it('can search in facet values, and reset pre post tags if needed', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
+ escapeFacetValues: false,
});
const helper = jsHelper({}, '', {
...widget.getConfiguration({}),
// Here we simulate that another widget has set some highlight tags
- ...tagConfig,
+ ...TAG_PLACEHOLDER,
});
helper.search = jest.fn();
helper.searchForFacetValues = jest.fn().mockReturnValue(
@@ -923,12 +1069,12 @@ describe('connectRefinementList', () => {
facetHits: [
{
count: 33,
- highlighted: 'Salvador Da li',
+ highlighted: 'Salvador Da li',
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: 'Da vidoff',
+ highlighted: 'Da vidoff',
value: 'Davidoff',
},
],
@@ -984,8 +1130,8 @@ describe('connectRefinementList', () => {
expect(sffvFacet).toBe('category');
expect(maxNbItems).toBe(2);
expect(paramOverride).toEqual({
- highlightPreTag: undefined,
- highlightPostTag: undefined,
+ highlightPreTag: '',
+ highlightPostTag: ' ',
});
return Promise.resolve().then(() => {
@@ -993,13 +1139,13 @@ describe('connectRefinementList', () => {
expect(rendering.mock.calls[2][0].items).toEqual([
{
count: 33,
- highlighted: 'Salvador Da li',
+ highlighted: 'Salvador Da li',
label: 'Salvador Dali',
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: 'Da vidoff',
+ highlighted: 'Da vidoff',
label: 'Davidoff',
value: 'Davidoff',
},
@@ -1007,10 +1153,10 @@ describe('connectRefinementList', () => {
});
});
- it('can search in facet values, and set post and pre tags if escapeFacetValues is true', () => {
+ it('can search in facet values, and set post and pre tags by default', () => {
const { makeWidget, rendering } = createWidgetFactory();
const widget = makeWidget({
- attributeName: 'category',
+ attribute: 'category',
limit: 2,
escapeFacetValues: true,
});
@@ -1018,7 +1164,7 @@ describe('connectRefinementList', () => {
const helper = jsHelper({}, '', {
...widget.getConfiguration({}),
// Here we simulate that another widget has set some highlight tags
- ...tagConfig,
+ ...TAG_PLACEHOLDER,
});
helper.search = jest.fn();
helper.searchForFacetValues = jest.fn().mockReturnValue(
@@ -1027,15 +1173,15 @@ describe('connectRefinementList', () => {
facetHits: [
{
count: 33,
- highlighted: `Salvador ${tagConfig.highlightPreTag}Da${
- tagConfig.highlightPostTag
+ highlighted: `Salvador ${TAG_PLACEHOLDER.highlightPreTag}Da${
+ TAG_PLACEHOLDER.highlightPostTag
}li`,
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: `${tagConfig.highlightPreTag}Da${
- tagConfig.highlightPostTag
+ highlighted: `${TAG_PLACEHOLDER.highlightPreTag}Da${
+ TAG_PLACEHOLDER.highlightPostTag
}vidoff`,
value: 'Davidoff',
},
@@ -1091,20 +1237,20 @@ describe('connectRefinementList', () => {
expect(sffvQuery).toBe('da');
expect(sffvFacet).toBe('category');
expect(maxNbItems).toBe(2);
- expect(paramOverride).toEqual(tagConfig);
+ expect(paramOverride).toEqual(TAG_PLACEHOLDER);
return Promise.resolve().then(() => {
expect(rendering).toHaveBeenCalledTimes(3);
expect(rendering.mock.calls[2][0].items).toEqual([
{
count: 33,
- highlighted: 'Salvador Da li',
+ highlighted: 'Salvador Da li',
label: 'Salvador Dali',
value: 'Salvador Dali',
},
{
count: 9,
- highlighted: 'Da vidoff',
+ highlighted: 'Da vidoff',
label: 'Davidoff',
value: 'Davidoff',
},
@@ -1118,7 +1264,7 @@ describe('connectRefinementList', () => {
const makeWidget = connectRefinementList(rendering);
const widget = makeWidget({
- attributeName: 'facetAttribute',
+ attribute: 'facetAttribute',
...config,
});
diff --git a/src/connectors/refinement-list/connectRefinementList.js b/src/connectors/refinement-list/connectRefinementList.js
index 6e16443157..39ebd15b41 100644
--- a/src/connectors/refinement-list/connectRefinementList.js
+++ b/src/connectors/refinement-list/connectRefinementList.js
@@ -1,5 +1,9 @@
import { checkRendering } from '../../lib/utils.js';
-import { tagConfig, escapeFacets } from '../../lib/escape-highlight.js';
+import {
+ escapeFacets,
+ TAG_PLACEHOLDER,
+ TAG_REPLACEMENT,
+} from '../../lib/escape-highlight.js';
import isEqual from 'lodash/isEqual';
const usage = `Usage:
@@ -20,38 +24,20 @@ var customRefinementList = connectRefinementList(function render(params) {
search.addWidget(
customRefinementList({
- attributeName,
+ attribute,
[ operator = 'or' ],
- [ limit ],
- [ showMoreLimit ],
+ [ limit = 10 ],
+ [ showMore = false ],
+ [ showMoreLimit = 20 ],
[ sortBy = ['isRefined', 'count:desc', 'name:asc'] ],
- [ escapeFacetValues = false ],
- [ transformItems ]
+ [ escapeFacetValues = true ],
+ [ transformItems ],
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectRefinementList.html
`;
-export const checkUsage = ({
- attributeName,
- operator,
- showMoreLimit,
- limit,
- message,
-}) => {
- const noAttributeName = attributeName === undefined;
- const invalidOperator = !/^(and|or)$/.test(operator);
- const invalidShowMoreLimit =
- showMoreLimit !== undefined
- ? isNaN(showMoreLimit) || showMoreLimit < limit
- : false;
-
- if (noAttributeName || invalidOperator || invalidShowMoreLimit) {
- throw new Error(message);
- }
-};
-
/**
* @typedef {Object} RefinementListItem
* @property {string} value The value of the refinement list item.
@@ -62,14 +48,15 @@ export const checkUsage = ({
/**
* @typedef {Object} CustomRefinementListWidgetOptions
- * @property {string} attributeName The name of the attribute in the records.
+ * @property {string} attribute The name of the attribute in the records.
* @property {"and"|"or"} [operator = 'or'] How the filters are combined together.
* @property {number} [limit = 10] The max number of items to display when
* `showMoreLimit` is not set or if the widget is showing less value.
- * @property {number} [showMoreLimit] The max number of items to display if the widget
+ * @property {boolean} [showMore = false] Whether to display a button that expands the number of items.
+ * @property {number} [showMoreLimit = 20] The max number of items to display if the widget
* is showing more items.
* @property {string[]|function} [sortBy = ['isRefined', 'count:desc', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
- * @property {boolean} [escapeFacetValues = false] Escapes the content of the facet values.
+ * @property {boolean} [escapeFacetValues = true] Escapes the content of the facet values.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -145,7 +132,7 @@ export const checkUsage = ({
* search.addWidget(
* customRefinementList({
* containerNode: $('#custom-refinement-list-container'),
- * attributeName: 'categories',
+ * attribute: 'categories',
* limit: 10,
* })
* );
@@ -155,22 +142,23 @@ export default function connectRefinementList(renderFn, unmountFn) {
return (widgetParams = {}) => {
const {
- attributeName,
+ attribute,
operator = 'or',
limit = 10,
- showMoreLimit,
+ showMore = false,
+ showMoreLimit = 20,
sortBy = ['isRefined', 'count:desc', 'name:asc'],
- escapeFacetValues = false,
+ escapeFacetValues = true,
transformItems = items => items,
} = widgetParams;
- checkUsage({
- message: usage,
- attributeName,
- operator,
- showMoreLimit,
- limit,
- });
+ if (!attribute || !/^(and|or)$/.test(operator)) {
+ throw new Error(usage);
+ }
+
+ if (showMore === true && showMoreLimit <= limit) {
+ throw new Error('`showMoreLimit` should be greater than `limit`.');
+ }
const formatItems = ({ name: label, ...item }) => ({
...item,
@@ -194,7 +182,7 @@ export default function connectRefinementList(renderFn, unmountFn) {
}) => {
// Compute a specific createURL method able to link to any facet value state change
const _createURL = facetValue =>
- createURL(state.toggleRefinement(attributeName, facetValue));
+ createURL(state.toggleRefinement(attribute, facetValue));
// Do not mistake searchForFacetValues and searchFacetValues which is the actual search
// function
@@ -219,9 +207,7 @@ export default function connectRefinementList(renderFn, unmountFn) {
canRefine: isFromSearch || items.length > 0,
widgetParams,
isShowingMore,
- canToggleShowMore: showMoreLimit
- ? isShowingMore || !hasExhaustiveItems
- : false,
+ canToggleShowMore: showMore && (isShowingMore || !hasExhaustiveItems),
toggleShowMore,
hasExhaustiveItems,
},
@@ -256,15 +242,15 @@ export default function connectRefinementList(renderFn, unmountFn) {
} else {
const tags = {
highlightPreTag: escapeFacetValues
- ? tagConfig.highlightPreTag
- : undefined,
+ ? TAG_PLACEHOLDER.highlightPreTag
+ : TAG_REPLACEMENT.highlightPreTag,
highlightPostTag: escapeFacetValues
- ? tagConfig.highlightPostTag
- : undefined,
+ ? TAG_PLACEHOLDER.highlightPostTag
+ : TAG_REPLACEMENT.highlightPostTag,
};
helper
- .searchForFacetValues(attributeName, query, limit, tags)
+ .searchForFacetValues(attribute, query, limit, tags)
.then(results => {
const facetValues = escapeFacetValues
? escapeFacets(results.facetHits)
@@ -316,26 +302,15 @@ export default function connectRefinementList(renderFn, unmountFn) {
getConfiguration: (configuration = {}) => {
const widgetConfiguration = {
- [operator === 'and' ? 'facets' : 'disjunctiveFacets']: [
- attributeName,
- ],
+ [operator === 'and' ? 'facets' : 'disjunctiveFacets']: [attribute],
};
- if (limit !== undefined) {
- const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
- if (showMoreLimit === undefined) {
- widgetConfiguration.maxValuesPerFacet = Math.max(
- currentMaxValuesPerFacet,
- limit
- );
- } else {
- widgetConfiguration.maxValuesPerFacet = Math.max(
- currentMaxValuesPerFacet,
- limit,
- showMoreLimit
- );
- }
- }
+ const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
+
+ widgetConfiguration.maxValuesPerFacet = Math.max(
+ currentMaxValuesPerFacet,
+ showMore ? showMoreLimit : limit
+ );
return widgetConfiguration;
},
@@ -344,7 +319,7 @@ export default function connectRefinementList(renderFn, unmountFn) {
this.cachedToggleShowMore = this.cachedToggleShowMore.bind(this);
refine = facetValue =>
- helper.toggleRefinement(attributeName, facetValue).search();
+ helper.toggleRefinement(attribute, facetValue).search();
searchForFacetValues = createSearchForFacetValues(helper);
@@ -371,7 +346,7 @@ export default function connectRefinementList(renderFn, unmountFn) {
instantSearchInstance,
} = renderOptions;
- const facetValues = results.getFacetValues(attributeName, { sortBy });
+ const facetValues = results.getFacetValues(attribute, { sortBy });
const items = transformItems(
facetValues.slice(0, this.getLimit()).map(formatItems)
);
@@ -414,26 +389,24 @@ export default function connectRefinementList(renderFn, unmountFn) {
unmountFn();
if (operator === 'and') {
- return state
- .removeFacetRefinement(attributeName)
- .removeFacet(attributeName);
+ return state.removeFacetRefinement(attribute).removeFacet(attribute);
} else {
return state
- .removeDisjunctiveFacetRefinement(attributeName)
- .removeDisjunctiveFacet(attributeName);
+ .removeDisjunctiveFacetRefinement(attribute)
+ .removeDisjunctiveFacet(attribute);
}
},
getWidgetState(uiState, { searchParameters }) {
const values =
operator === 'or'
- ? searchParameters.getDisjunctiveRefinements(attributeName)
- : searchParameters.getConjunctiveRefinements(attributeName);
+ ? searchParameters.getDisjunctiveRefinements(attribute)
+ : searchParameters.getConjunctiveRefinements(attribute);
if (
values.length === 0 ||
(uiState.refinementList &&
- isEqual(values, uiState.refinementList[attributeName]))
+ isEqual(values, uiState.refinementList[attribute]))
) {
return uiState;
}
@@ -442,21 +415,21 @@ export default function connectRefinementList(renderFn, unmountFn) {
...uiState,
refinementList: {
...uiState.refinementList,
- [attributeName]: values,
+ [attribute]: values,
},
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
const values =
- uiState.refinementList && uiState.refinementList[attributeName];
+ uiState.refinementList && uiState.refinementList[attribute];
if (values === undefined) return searchParameters;
return values.reduce(
(sp, v) =>
operator === 'or'
- ? sp.addDisjunctiveFacetRefinement(attributeName, v)
- : sp.addFacetRefinement(attributeName, v),
- searchParameters.clearRefinements(attributeName)
+ ? sp.addDisjunctiveFacetRefinement(attribute, v)
+ : sp.addFacetRefinement(attribute, v),
+ searchParameters.clearRefinements(attribute)
);
},
};
diff --git a/src/connectors/sort-by-selector/__tests__/__snapshots__/connectSortBySelector-test.js.snap b/src/connectors/sort-by/__tests__/__snapshots__/connectSortBy-test.js.snap
similarity index 90%
rename from src/connectors/sort-by-selector/__tests__/__snapshots__/connectSortBySelector-test.js.snap
rename to src/connectors/sort-by/__tests__/__snapshots__/connectSortBy-test.js.snap
index 8b77d14d0a..08ef57dd9c 100644
--- a/src/connectors/sort-by-selector/__tests__/__snapshots__/connectSortBySelector-test.js.snap
+++ b/src/connectors/sort-by/__tests__/__snapshots__/connectSortBy-test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`connectSortBySelector routing getWidgetSearchParameters should add the refinements according to the UI state provided 1`] = `
+exports[`connectSortBy routing getWidgetSearchParameters should add the refinements according to the UI state provided 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -57,7 +57,7 @@ SearchParameters {
}
`;
-exports[`connectSortBySelector routing getWidgetSearchParameters should enforce the default value 1`] = `
+exports[`connectSortBy routing getWidgetSearchParameters should enforce the default value 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -114,7 +114,7 @@ SearchParameters {
}
`;
-exports[`connectSortBySelector routing getWidgetState should add an entry equal to the refinement 1`] = `
+exports[`connectSortBy routing getWidgetState should add an entry equal to the refinement 1`] = `
Object {
"sortBy": "priceASC",
}
diff --git a/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js b/src/connectors/sort-by/__tests__/connectSortBy-test.js
similarity index 84%
rename from src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js
rename to src/connectors/sort-by/__tests__/connectSortBy-test.js
index 62ed795dfb..32e413e3aa 100644
--- a/src/connectors/sort-by-selector/__tests__/connectSortBySelector-test.js
+++ b/src/connectors/sort-by/__tests__/connectSortBy-test.js
@@ -3,29 +3,29 @@ import jsHelper, {
SearchParameters,
} from 'algoliasearch-helper';
-import connectSortBySelector from '../connectSortBySelector.js';
+import connectSortBy from '../connectSortBy.js';
import instantSearch from '../../../lib/main.js';
-describe('connectSortBySelector', () => {
+describe('connectSortBy', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
const rendering = jest.fn();
- const makeWidget = connectSortBySelector(rendering);
+ const makeWidget = connectSortBy(rendering);
const instantSearchInstance = instantSearch({
indexName: 'defaultIndex',
searchClient: { search() {} },
});
- const indices = [
- { label: 'Sort products by relevance', name: 'relevance' },
- { label: 'Sort products by price', name: 'priceASC' },
+ const items = [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
];
- const widget = makeWidget({ indices });
+ const widget = makeWidget({ items });
expect(widget.getConfiguration).toBe(undefined);
- const helper = jsHelper({}, indices[0].name);
+ const helper = jsHelper({}, items[0].value);
helper.search = jest.fn();
widget.init({
@@ -41,7 +41,7 @@ describe('connectSortBySelector', () => {
expect(rendering).toHaveBeenLastCalledWith(
expect.objectContaining({
currentRefinement: helper.state.index,
- widgetParams: { indices },
+ widgetParams: { items },
options: [
{ label: 'Sort products by relevance', value: 'relevance' },
{ label: 'Sort products by price', value: 'priceASC' },
@@ -62,7 +62,7 @@ describe('connectSortBySelector', () => {
expect(rendering).toHaveBeenLastCalledWith(
expect.objectContaining({
currentRefinement: helper.state.index,
- widgetParams: { indices },
+ widgetParams: { items },
options: [
{ label: 'Sort products by relevance', value: 'relevance' },
{ label: 'Sort products by price', value: 'priceASC' },
@@ -74,23 +74,23 @@ describe('connectSortBySelector', () => {
it('Renders with transformed items', () => {
const rendering = jest.fn();
- const makeWidget = connectSortBySelector(rendering);
+ const makeWidget = connectSortBy(rendering);
const instantSearchInstance = instantSearch({
indexName: 'defaultIndex',
searchClient: { search() {} },
});
- const indices = [
- { label: 'Sort products by relevance', name: 'relevance' },
- { label: 'Sort products by price', name: 'priceASC' },
+ const items = [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
];
const widget = makeWidget({
- indices,
- transformItems: items =>
- items.map(item => ({ ...item, label: 'transformed' })),
+ items,
+ transformItems: allItems =>
+ allItems.map(item => ({ ...item, label: 'transformed' })),
});
- const helper = jsHelper({}, indices[0].name);
+ const helper = jsHelper({}, items[0].value);
helper.search = jest.fn();
widget.init({
@@ -129,21 +129,21 @@ describe('connectSortBySelector', () => {
it('Provides a function to update the index at each step', () => {
const rendering = jest.fn();
- const makeWidget = connectSortBySelector(rendering);
+ const makeWidget = connectSortBy(rendering);
const instantSearchInstance = instantSearch({
indexName: 'defaultIndex',
searchClient: { search() {} },
});
- const indices = [
- { label: 'Sort products by relevance', name: 'relevance' },
- { label: 'Sort products by price', name: 'priceASC' },
+ const items = [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
];
const widget = makeWidget({
- indices,
+ items,
});
- const helper = jsHelper({}, indices[0].name);
+ const helper = jsHelper({}, items[0].value);
helper.search = jest.fn();
widget.init({
@@ -156,7 +156,7 @@ describe('connectSortBySelector', () => {
{
// first rendering
- expect(helper.state.index).toBe(indices[0].name);
+ expect(helper.state.index).toBe(items[0].value);
const renderOptions =
rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, currentRefinement } = renderOptions;
@@ -189,19 +189,19 @@ describe('connectSortBySelector', () => {
describe('routing', () => {
const getInitializedWidget = (config = {}) => {
const rendering = jest.fn();
- const makeWidget = connectSortBySelector(rendering);
+ const makeWidget = connectSortBy(rendering);
const instantSearchInstance = instantSearch({
indexName: 'relevance',
searchClient: { search() {} },
});
- const indices = [
- { label: 'Sort products by relevance', name: 'relevance' },
- { label: 'Sort products by price', name: 'priceASC' },
- { label: 'Sort products by magic', name: 'other' },
+ const items = [
+ { label: 'Sort products by relevance', value: 'relevance' },
+ { label: 'Sort products by price', value: 'priceASC' },
+ { label: 'Sort products by magic', value: 'other' },
];
const widget = makeWidget({
- indices,
+ items,
...config,
});
diff --git a/src/connectors/sort-by-selector/connectSortBySelector.js b/src/connectors/sort-by/connectSortBy.js
similarity index 56%
rename from src/connectors/sort-by-selector/connectSortBySelector.js
rename to src/connectors/sort-by/connectSortBy.js
index fd959abab3..fed5a70249 100644
--- a/src/connectors/sort-by-selector/connectSortBySelector.js
+++ b/src/connectors/sort-by/connectSortBy.js
@@ -2,7 +2,7 @@ import find from 'lodash/find';
import { checkRendering } from '../../lib/utils.js';
const usage = `Usage:
-var customSortBySelector = connectSortBySelector(function render(params, isFirstRendering) {
+var customSortBy = connectSortBy(function render(params, isFirstRendering) {
// params = {
// currentRefinement,
// options,
@@ -13,37 +13,37 @@ var customSortBySelector = connectSortBySelector(function render(params, isFirst
// }
});
search.addWidget(
- customSortBySelector({
- indices,
+ customSortBy({
+ items,
[ transformItems ]
})
);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectSortBySelector.html
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectSortBy.html
`;
/**
- * @typedef {Object} SortBySelectorIndices
- * @property {string} name Name of the index to target.
- * @property {string} label Label to display for the targeted index.
+ * @typedef {Object} SortByItem
+ * @property {string} value The name of the index to target.
+ * @property {string} label The label of the index to display.
*/
/**
- * @typedef {Object} CustomSortBySelectorWidgetOptions
- * @property {SortBySelectorIndices[]} indices Array of objects defining the different indices to choose from.
+ * @typedef {Object} CustomSortByWidgetOptions
+ * @property {SortByItem[]} items Array of objects defining the different indices to choose from.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
- * @typedef {Object} SortBySelectorRenderingOptions
+ * @typedef {Object} SortByRenderingOptions
* @property {string} currentRefinement The currently selected index.
- * @property {SortBySelectorIndices[]} options All the available indices
+ * @property {SortByItem[]} options All the available indices
* @property {function(string)} refine Switches indices and triggers a new search.
* @property {boolean} hasNoResults `true` if the last search contains no result.
- * @property {Object} widgetParams All original `CustomSortBySelectorWidgetOptions` forwarded to the `renderFn`.
+ * @property {Object} widgetParams All original `CustomSortByWidgetOptions` forwarded to the `renderFn`.
*/
/**
- * The **SortBySelector** connector provides the logic to build a custom widget that will display a
+ * The **SortBy** connector provides the logic to build a custom widget that will display a
* list of indices. With Algolia, this is most commonly used for changing ranking strategy. This allows
* a user to change how the hits are being sorted.
*
@@ -52,78 +52,70 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* `options` that are the values that can be selected. `refine` should be used
* with `options.value`.
* @type {Connector}
- * @param {function(SortBySelectorRenderingOptions, boolean)} renderFn Rendering function for the custom **SortBySelector** widget.
+ * @param {function(SortByRenderingOptions, boolean)} renderFn Rendering function for the custom **SortBy** widget.
* @param {function} unmountFn Unmount function called when the widget is disposed.
- * @return {function(CustomSortBySelectorWidgetOptions)} Re-usable widget factory for a custom **SortBySelector** widget.
+ * @return {function(CustomSortByWidgetOptions)} Re-usable widget factory for a custom **SortBy** widget.
* @example
- * // custom `renderFn` to render the custom SortBySelector widget
- * function renderFn(SortBySelectorRenderingOptions, isFirstRendering) {
+ * // custom `renderFn` to render the custom SortBy widget
+ * function renderFn(SortByRenderingOptions, isFirstRendering) {
* if (isFirstRendering) {
- * SortBySelectorRenderingOptions.widgetParams.containerNode.html(' ');
- * SortBySelectorRenderingOptions.widgetParams.containerNode
+ * SortByRenderingOptions.widgetParams.containerNode.html(' ');
+ * SortByRenderingOptions.widgetParams.containerNode
* .find('select')
* .on('change', function(event) {
- * SortBySelectorRenderingOptions.refine(event.target.value);
+ * SortByRenderingOptions.refine(event.target.value);
* });
* }
*
- * var optionsHTML = SortBySelectorRenderingOptions.options.map(function(option) {
+ * var optionsHTML = SortByRenderingOptions.options.map(function(option) {
* return `
*
* ${option.label}
*
* `;
* });
*
- * SortBySelectorRenderingOptions.widgetParams.containerNode
+ * SortByRenderingOptions.widgetParams.containerNode
* .find('select')
* .html(optionsHTML);
* }
*
- * // connect `renderFn` to SortBySelector logic
- * var customSortBySelector = instantsearch.connectors.connectSortBySelector(renderFn);
+ * // connect `renderFn` to SortBy logic
+ * var customSortBy = instantsearch.connectors.connectSortBy(renderFn);
*
* // mount widget on the page
* search.addWidget(
- * customSortBySelector({
- * containerNode: $('#custom-sort-by-selector-container'),
- * indices: [
- * {name: 'instant_search', label: 'Most relevant'},
- * {name: 'instant_search_price_asc', label: 'Lowest price'},
- * {name: 'instant_search_price_desc', label: 'Highest price'},
+ * customSortBy({
+ * containerNode: $('#custom-sort-by-container'),
+ * items: [
+ * { value: 'instant_search', label: 'Most relevant' },
+ * { value: 'instant_search_price_asc', label: 'Lowest price' },
+ * { value: 'instant_search_price_desc', label: 'Highest price' },
* ],
* })
* );
*/
-export default function connectSortBySelector(renderFn, unmountFn) {
+export default function connectSortBy(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const { indices, transformItems = items => items } = widgetParams;
+ const { items, transformItems = x => x } = widgetParams;
- if (!indices) {
+ if (!items) {
throw new Error(usage);
}
- const selectorOptions = indices.map(({ label, name }) => ({
- label,
- value: name,
- }));
-
return {
init({ helper, instantSearchInstance }) {
const currentIndex = helper.getIndex();
- const isIndexInList = find(
- indices,
- ({ name }) => name === currentIndex
- );
+ const isIndexInList = find(items, item => item.value === currentIndex);
if (!isIndexInList) {
throw new Error(
- `[sortBySelector]: Index ${currentIndex} not present in \`indices\``
+ `[sortBy]: Index ${currentIndex} not present in \`items\``
);
}
@@ -133,7 +125,7 @@ export default function connectSortBySelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: currentIndex,
- options: transformItems(selectorOptions),
+ options: transformItems(items),
refine: this.setIndex,
hasNoResults: true,
widgetParams,
@@ -147,7 +139,7 @@ export default function connectSortBySelector(renderFn, unmountFn) {
renderFn(
{
currentRefinement: helper.getIndex(),
- options: transformItems(selectorOptions),
+ options: transformItems(items),
refine: this.setIndex,
hasNoResults: results.nbHits === 0,
widgetParams,
diff --git a/src/connectors/stats/__tests__/connectStats-test.js b/src/connectors/stats/__tests__/connectStats-test.js
index 57cc2182ff..36bf43c371 100644
--- a/src/connectors/stats/__tests__/connectStats-test.js
+++ b/src/connectors/stats/__tests__/connectStats-test.js
@@ -1,5 +1,3 @@
-import sinon from 'sinon';
-
import jsHelper from 'algoliasearch-helper';
const SearchResults = jsHelper.SearchResults;
@@ -9,7 +7,7 @@ describe('connectStats', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
+ const rendering = jest.fn();
const makeWidget = connectStats(rendering);
const widget = makeWidget({
@@ -19,7 +17,7 @@ describe('connectStats', () => {
expect(widget.getConfiguration).toEqual(undefined);
const helper = jsHelper({});
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -30,8 +28,9 @@ describe('connectStats', () => {
{
// should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(1);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(true);
// should provide good values for the first rendering
@@ -43,7 +42,7 @@ describe('connectStats', () => {
processingTimeMS,
query,
widgetParams,
- } = rendering.lastCall.args[0];
+ } = rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(hitsPerPage).toBe(helper.state.hitsPerPage);
expect(nbHits).toBe(0);
expect(nbPages).toBe(0);
@@ -72,8 +71,9 @@ describe('connectStats', () => {
{
// Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(2);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(false);
// should provide good values after the first search
@@ -84,7 +84,7 @@ describe('connectStats', () => {
page,
processingTimeMS,
query,
- } = rendering.lastCall.args[0];
+ } = rendering.mock.calls[rendering.mock.calls.length - 1][0];
expect(hitsPerPage).toBe(helper.state.hitsPerPage);
expect(nbHits).toBe(1);
expect(nbPages).toBe(1);
diff --git a/src/connectors/toggle/__tests__/__snapshots__/connectToggle-test.js.snap b/src/connectors/toggleRefinement/__tests__/__snapshots__/connectToggleRefinement-test.js.snap
similarity index 91%
rename from src/connectors/toggle/__tests__/__snapshots__/connectToggle-test.js.snap
rename to src/connectors/toggleRefinement/__tests__/__snapshots__/connectToggleRefinement-test.js.snap
index 402cf05bee..5212a61844 100644
--- a/src/connectors/toggle/__tests__/__snapshots__/connectToggle-test.js.snap
+++ b/src/connectors/toggleRefinement/__tests__/__snapshots__/connectToggleRefinement-test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`connectToggle routing getWidgetSearchParameters should remove the refinement (one value) 1`] = `
+exports[`connectToggleRefinement routing getWidgetSearchParameters should remove the refinement (one value) 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -59,7 +59,7 @@ SearchParameters {
}
`;
-exports[`connectToggle routing getWidgetSearchParameters should update the SP base on the UI state (two values) 1`] = `
+exports[`connectToggleRefinement routing getWidgetSearchParameters should update the SP base on the UI state (two values) 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -122,7 +122,7 @@ SearchParameters {
}
`;
-exports[`connectToggle routing getWidgetSearchParameters should update the SP base on the UI state - toggled (two values) 1`] = `
+exports[`connectToggleRefinement routing getWidgetSearchParameters should update the SP base on the UI state - toggled (two values) 1`] = `
SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -185,7 +185,7 @@ SearchParameters {
}
`;
-exports[`connectToggle routing getWidgetState should add an entry equal to the refinement 1`] = `
+exports[`connectToggleRefinement routing getWidgetState should add an entry equal to the refinement 1`] = `
Object {
"toggle": Object {
"isShippingFree": true,
diff --git a/src/connectors/toggle/__tests__/connectToggle-test.js b/src/connectors/toggleRefinement/__tests__/connectToggleRefinement-test.js
similarity index 80%
rename from src/connectors/toggle/__tests__/connectToggle-test.js
rename to src/connectors/toggleRefinement/__tests__/connectToggleRefinement-test.js
index b3e7da4d78..975f256151 100644
--- a/src/connectors/toggle/__tests__/connectToggle-test.js
+++ b/src/connectors/toggleRefinement/__tests__/connectToggleRefinement-test.js
@@ -1,33 +1,29 @@
-import sinon from 'sinon';
-
import jsHelper, {
SearchResults,
SearchParameters,
} from 'algoliasearch-helper';
-import connectToggle from '../connectToggle.js';
+import connectToggleRefinement from '../connectToggleRefinement.js';
-describe('connectToggle', () => {
+describe('connectToggleRefinement', () => {
it('Renders during init and render', () => {
// test that the dummyRendering is called with the isFirstRendering
// flag set accordingly
- const rendering = sinon.stub();
- const makeWidget = connectToggle(rendering);
+ const rendering = jest.fn();
+ const makeWidget = connectToggleRefinement(rendering);
- const attributeName = 'isShippingFree';
- const label = 'Free shipping?';
+ const attribute = 'isShippingFree';
const widget = makeWidget({
- attributeName,
- label,
+ attribute,
});
const config = widget.getConfiguration();
expect(config).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
const helper = jsHelper({}, '', config);
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -38,31 +34,31 @@ describe('connectToggle', () => {
{
// should call the rendering once with isFirstRendering to true
- expect(rendering.callCount).toBe(1);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(1);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(true);
// should provide good values for the first rendering
- const { value, widgetParams } = rendering.lastCall.args[0];
+ const { value, widgetParams } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: null,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 0,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 0,
},
});
expect(widgetParams).toEqual({
- attributeName,
- label,
+ attribute,
});
}
@@ -85,23 +81,24 @@ describe('connectToggle', () => {
{
// Should call the rendering a second time, with isFirstRendering to false
- expect(rendering.callCount).toBe(2);
- const isFirstRendering = rendering.lastCall.args[1];
+ expect(rendering).toHaveBeenCalledTimes(2);
+ const isFirstRendering =
+ rendering.mock.calls[rendering.mock.calls.length - 1][1];
expect(isFirstRendering).toBe(false);
// should provide good values after the first search
- const { value } = rendering.lastCall.args[0];
+ const { value } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: 45,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 45,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 85,
},
@@ -110,18 +107,16 @@ describe('connectToggle', () => {
});
it('Provides a function to add/remove a facet value', () => {
- const rendering = sinon.stub();
- const makeWidget = connectToggle(rendering);
+ const rendering = jest.fn();
+ const makeWidget = connectToggleRefinement(rendering);
- const attributeName = 'isShippingFree';
- const label = 'Free shipping?';
+ const attribute = 'isShippingFree';
const widget = makeWidget({
- attributeName,
- label,
+ attribute,
});
const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -132,32 +127,31 @@ describe('connectToggle', () => {
{
// first rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual(
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual(
undefined
);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: null,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 0,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 0,
},
});
refine({ isRefined: value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
refine({ isRefined: !value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual(
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual(
undefined
);
}
@@ -181,28 +175,27 @@ describe('connectToggle', () => {
{
// Second rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual(
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual(
undefined
);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: 45,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 45,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 85,
},
});
refine({ isRefined: value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
}
@@ -234,50 +227,45 @@ describe('connectToggle', () => {
{
// Third rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: 85,
isRefined: true,
onFacetValue: {
- name: label,
isRefined: true,
count: 45,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 85,
},
});
refine(value);
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual(
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual(
undefined
);
}
});
it('Provides a function to toggle between two values', () => {
- const rendering = sinon.stub();
- const makeWidget = connectToggle(rendering);
+ const rendering = jest.fn();
+ const makeWidget = connectToggleRefinement(rendering);
- const attributeName = 'isShippingFree';
- const label = 'Free shipping?';
+ const attribute = 'isShippingFree';
const widget = makeWidget({
- attributeName,
- label,
- values: {
- on: 'true',
- off: 'false',
- },
+ attribute,
+ on: 'true',
+ off: 'false',
});
const helper = jsHelper({}, '', widget.getConfiguration());
- helper.search = sinon.stub();
+ helper.search = jest.fn();
widget.init({
helper,
@@ -288,33 +276,32 @@ describe('connectToggle', () => {
{
// first rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'false',
]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: null,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 0,
},
offFacetValue: {
- name: label,
isRefined: true,
count: 0,
},
});
refine({ isRefined: value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
refine({ isRefined: !value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'false',
]);
}
@@ -346,29 +333,28 @@ describe('connectToggle', () => {
{
// Second rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'false',
]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
// the value is the one that is not selected
+ name: 'isShippingFree',
count: 45,
isRefined: false,
onFacetValue: {
- name: label,
isRefined: false,
count: 45,
},
offFacetValue: {
- name: label,
isRefined: true,
count: 40,
},
});
refine({ isRefined: value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
}
@@ -400,28 +386,27 @@ describe('connectToggle', () => {
{
// Third rendering
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'true',
]);
- const renderOptions = rendering.lastCall.args[0];
+ const renderOptions =
+ rendering.mock.calls[rendering.mock.calls.length - 1][0];
const { refine, value } = renderOptions;
expect(value).toEqual({
- name: label,
+ name: 'isShippingFree',
count: 40,
isRefined: true,
onFacetValue: {
- name: label,
isRefined: true,
count: 45,
},
offFacetValue: {
- name: label,
isRefined: false,
count: 40,
},
});
refine({ isRefined: value.isRefined });
- expect(helper.state.disjunctiveFacetsRefinements[attributeName]).toEqual([
+ expect(helper.state.disjunctiveFacetsRefinements[attribute]).toEqual([
'false',
]);
}
@@ -430,13 +415,11 @@ describe('connectToggle', () => {
describe('routing', () => {
const getInitializedWidget = (config = {}) => {
const rendering = jest.fn();
- const makeWidget = connectToggle(rendering);
+ const makeWidget = connectToggleRefinement(rendering);
- const attributeName = 'isShippingFree';
- const label = 'Free shipping?';
+ const attribute = 'isShippingFree';
const widget = makeWidget({
- attributeName,
- label,
+ attribute,
...config,
});
@@ -451,7 +434,9 @@ describe('connectToggle', () => {
onHistoryChange: () => {},
});
- const { refine } = rendering.mock.calls[0][0];
+ const { refine } = rendering.mock.calls[
+ rendering.mock.calls.length - 1
+ ][0];
return [widget, helper, refine];
};
@@ -510,10 +495,8 @@ describe('connectToggle', () => {
test('should enforce the default value if no value is in the UI state (two values)', () => {
const [widget, helper] = getInitializedWidget({
- values: {
- on: 'free-shipping',
- off: 'paid-shipping',
- },
+ on: 'free-shipping',
+ off: 'paid-shipping',
});
const uiState = {};
const searchParametersBefore = SearchParameters.make(helper.state);
@@ -538,10 +521,8 @@ describe('connectToggle', () => {
test('should update the SP base on the UI state (two values)', () => {
const [widget, helper, refine] = getInitializedWidget({
- values: {
- on: 'free-shipping',
- off: 'paid-shipping',
- },
+ on: 'free-shipping',
+ off: 'paid-shipping',
});
refine({ isRefined: false });
const uiState = {};
@@ -555,10 +536,8 @@ describe('connectToggle', () => {
test('should update the SP base on the UI state - toggled (two values)', () => {
const [widget, helper] = getInitializedWidget({
- values: {
- on: 'free-shipping',
- off: 'paid-shipping',
- },
+ on: 'free-shipping',
+ off: 'paid-shipping',
});
const uiState = {
toggle: {
diff --git a/src/connectors/toggle/connectToggle.js b/src/connectors/toggleRefinement/connectToggleRefinement.js
similarity index 73%
rename from src/connectors/toggle/connectToggle.js
rename to src/connectors/toggleRefinement/connectToggleRefinement.js
index c1719e015f..4c7e13d659 100644
--- a/src/connectors/toggle/connectToggle.js
+++ b/src/connectors/toggleRefinement/connectToggleRefinement.js
@@ -7,7 +7,7 @@ import {
import find from 'lodash/find';
const usage = `Usage:
-var customToggle = connectToggle(function render(params, isFirstRendering) {
+var customToggle = connectToggleRefinement(function render(params, isFirstRendering) {
// params = {
// value,
// createURL,
@@ -18,17 +18,16 @@ var customToggle = connectToggle(function render(params, isFirstRendering) {
});
search.addWidget(
customToggle({
- attributeName,
- label,
- [ values = {on: true, off: undefined} ]
+ attribute,
+ [on = true],
+ [off],
})
);
-Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectToggle.html
+Full documentation available at https://community.algolia.com/instantsearch.js/v2/connectors/connectToggleRefinement.html
`;
/**
* @typedef {Object} ToggleValue
- * @property {string} name Human-readable name of the filter.
* @property {boolean} isRefined `true` if the toggle is on.
* @property {number} count Number of results matched after applying the toggle refinement.
* @property {Object} onFacetValue Value of the toggle when it's on.
@@ -37,15 +36,15 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {Object} CustomToggleWidgetOptions
- * @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
- * @property {string} label Human-readable name of the filter (eg. "Free Shipping").
- * @property {Object} [values = {on: true, off: undefined}] Values to filter on when toggling.
+ * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping").
+ * @property {Object} [on = true] Value to filter on when toggled.
+ * @property {Object} [off] Value to filter on when not toggled.
*/
/**
* @typedef {Object} ToggleRenderingOptions
* @property {ToggleValue} value The current toggle value.
- * @property {function(): string} createURL Creates an URL for the next state.
+ * @property {function():string} createURL Creates an URL for the next state.
* @property {function(value)} refine Updates to the next state by applying the toggle refinement.
* @property {Object} widgetParams All original `CustomToggleWidgetOptions` forwarded to the `renderFn`.
*/
@@ -92,39 +91,34 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* }
*
* // connect `renderFn` to Toggle logic
- * var customToggle = instantsearch.connectors.connectToggle(renderFn);
+ * var customToggle = instantsearch.connectors.connectToggleRefinement(renderFn);
*
* // mount widget on the page
* search.addWidget(
* customToggle({
* containerNode: $('#custom-toggle-container'),
- * attributeName: 'free_shipping',
- * label: 'Free Shipping (toggle single value)',
+ * attribute: 'free_shipping',
* })
* );
*/
-export default function connectToggle(renderFn, unmountFn) {
+export default function connectToggleRefinement(renderFn, unmountFn) {
checkRendering(renderFn, usage);
return (widgetParams = {}) => {
- const {
- attributeName,
- label,
- values: userValues = { on: true, off: undefined },
- } = widgetParams;
+ const { attribute, on: userOn = true, off: userOff } = widgetParams;
- if (!attributeName || !label) {
+ if (!attribute) {
throw new Error(usage);
}
- const hasAnOffValue = userValues.off !== undefined;
- const on = userValues ? escapeRefinement(userValues.on) : undefined;
- const off = userValues ? escapeRefinement(userValues.off) : undefined;
+ const hasAnOffValue = userOff !== undefined;
+ const on = userOn ? escapeRefinement(userOn) : undefined;
+ const off = userOff ? escapeRefinement(userOff) : undefined;
return {
getConfiguration() {
return {
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
};
},
@@ -132,14 +126,14 @@ export default function connectToggle(renderFn, unmountFn) {
// Checking
if (!isRefined) {
if (hasAnOffValue) {
- helper.removeDisjunctiveFacetRefinement(attributeName, off);
+ helper.removeDisjunctiveFacetRefinement(attribute, off);
}
- helper.addDisjunctiveFacetRefinement(attributeName, on);
+ helper.addDisjunctiveFacetRefinement(attribute, on);
} else {
// Unchecking
- helper.removeDisjunctiveFacetRefinement(attributeName, on);
+ helper.removeDisjunctiveFacetRefinement(attribute, on);
if (hasAnOffValue) {
- helper.addDisjunctiveFacetRefinement(attributeName, off);
+ helper.addDisjunctiveFacetRefinement(attribute, off);
}
}
@@ -151,11 +145,11 @@ export default function connectToggle(renderFn, unmountFn) {
createURL(
state
.removeDisjunctiveFacetRefinement(
- attributeName,
+ attribute,
isCurrentlyRefined ? on : off
)
.addDisjunctiveFacetRefinement(
- attributeName,
+ attribute,
isCurrentlyRefined ? off : on
)
);
@@ -164,7 +158,7 @@ export default function connectToggle(renderFn, unmountFn) {
this._toggleRefinement(helper, opts);
};
- const isRefined = state.isDisjunctiveFacetRefined(attributeName, on);
+ const isRefined = state.isDisjunctiveFacetRefined(attribute, on);
// no need to refine anything at init if no custom off values
if (hasAnOffValue) {
@@ -172,25 +166,23 @@ export default function connectToggle(renderFn, unmountFn) {
if (!isRefined) {
const currentPage = helper.getPage();
helper
- .addDisjunctiveFacetRefinement(attributeName, off)
+ .addDisjunctiveFacetRefinement(attribute, off)
.setPage(currentPage);
}
}
const onFacetValue = {
- name: label,
isRefined,
count: 0,
};
const offFacetValue = {
- name: label,
isRefined: hasAnOffValue && !isRefined,
count: 0,
};
const value = {
- name: label,
+ name: attribute,
isRefined,
count: null,
onFacetValue,
@@ -210,19 +202,15 @@ export default function connectToggle(renderFn, unmountFn) {
},
render({ helper, results, state, instantSearchInstance }) {
- const isRefined = helper.state.isDisjunctiveFacetRefined(
- attributeName,
- on
- );
+ const isRefined = helper.state.isDisjunctiveFacetRefined(attribute, on);
const offValue = off === undefined ? false : off;
- const allFacetValues = results.getFacetValues(attributeName);
+ const allFacetValues = results.getFacetValues(attribute);
const onData = find(
allFacetValues,
({ name }) => name === unescapeRefinement(on)
);
const onFacetValue = {
- name: label,
isRefined: onData !== undefined ? onData.isRefined : false,
count: onData === undefined ? null : onData.count,
};
@@ -234,7 +222,6 @@ export default function connectToggle(renderFn, unmountFn) {
)
: undefined;
const offFacetValue = {
- name: label,
isRefined: offData !== undefined ? offData.isRefined : false,
count:
offData === undefined
@@ -248,7 +235,7 @@ export default function connectToggle(renderFn, unmountFn) {
const nextRefinement = isRefined ? offFacetValue : onFacetValue;
const value = {
- name: label,
+ name: attribute,
isRefined,
count: nextRefinement === undefined ? null : nextRefinement.count,
onFacetValue,
@@ -273,23 +260,21 @@ export default function connectToggle(renderFn, unmountFn) {
unmountFn();
const nextState = state
- .removeDisjunctiveFacetRefinement(attributeName)
- .removeDisjunctiveFacet(attributeName);
+ .removeDisjunctiveFacetRefinement(attribute)
+ .removeDisjunctiveFacet(attribute);
return nextState;
},
getWidgetState(uiState, { searchParameters }) {
const isRefined = searchParameters.isDisjunctiveFacetRefined(
- attributeName,
+ attribute,
on
);
if (
!isRefined ||
- (uiState &&
- uiState.toggle &&
- uiState.toggle[attributeName] === isRefined)
+ (uiState && uiState.toggle && uiState.toggle[attribute] === isRefined)
) {
return uiState;
}
@@ -298,37 +283,29 @@ export default function connectToggle(renderFn, unmountFn) {
...uiState,
toggle: {
...uiState.toggle,
- [attributeName]: isRefined,
+ [attribute]: isRefined,
},
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
- const isRefined = Boolean(
- uiState.toggle && uiState.toggle[attributeName]
- );
+ const isRefined = Boolean(uiState.toggle && uiState.toggle[attribute]);
if (isRefined) {
if (hasAnOffValue)
return searchParameters
- .removeDisjunctiveFacetRefinement(attributeName, off)
- .addDisjunctiveFacetRefinement(attributeName, on);
+ .removeDisjunctiveFacetRefinement(attribute, off)
+ .addDisjunctiveFacetRefinement(attribute, on);
- return searchParameters.addDisjunctiveFacetRefinement(
- attributeName,
- on
- );
+ return searchParameters.addDisjunctiveFacetRefinement(attribute, on);
}
if (hasAnOffValue)
return searchParameters
- .removeDisjunctiveFacetRefinement(attributeName, on)
- .addDisjunctiveFacetRefinement(attributeName, off);
+ .removeDisjunctiveFacetRefinement(attribute, on)
+ .addDisjunctiveFacetRefinement(attribute, off);
- return searchParameters.removeDisjunctiveFacetRefinement(
- attributeName,
- on
- );
+ return searchParameters.removeDisjunctiveFacetRefinement(attribute, on);
},
};
};
diff --git a/src/css/_base.scss b/src/css/_base.scss
deleted file mode 100644
index ae28d3f19c..0000000000
--- a/src/css/_base.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-@mixin block($block) {
- .ais-#{$block} {
- @content;
- }
-}
-
-@mixin element($element) {
- &--#{$element} {
- @content;
- }
-}
-
-@mixin modifier($modifier) {
- &__#{$modifier} {
- @content;
- }
-}
-
-@mixin bem($block, $element, $modifier: "") {
- @include block($block) {
- @include element($element) {
- @if $modifier != "" {
- @include modifier($modifier) {
- @content;
- }
- } @else {
- @content;
- }
- }
- }
-}
diff --git a/src/css/debug.css b/src/css/debug.css
deleted file mode 100644
index 22628c72dc..0000000000
--- a/src/css/debug.css
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * This file can be included in your page to help you debug which `ais-*`
- * classes are added to the widgets.
- * It will outline every added node and display the class name on hover
- **/
-[class^=ais-] {
- outline: 1px solid red !important;
- position: relative;
-}
-[class^=ais-]:hover:after {
- background: red;
- color: black;
- content: attr(class);
- font-size: 1rem;
- height: 20px;
- left: 0;
- padding: .5rem;
- position: absolute;
- top: 0;
- white-space: nowrap;
- z-index: 10;
- font-weight: normal;
-}
-[class^=ais-] [class^=ais-] {
- outline: 1px solid orange !important;
- position: relative;
-}
-[class^=ais-] [class^=ais-]:hover:after {
- background: orange;
- top: 20px;
- z-index: 100;
-}
-[class^=ais-] [class^=ais-] [class^=ais-] {
- outline: 1px solid yellow !important;
- position: relative;
-}
-[class^=ais-] [class^=ais-] [class^=ais-]:hover:after {
- background: yellow;
- top: 40px;
- z-index: 1000;
-}
-[class^=ais-] [class^=ais-] [class^=ais-] [class^=ais-] {
- outline: 1px solid cyan !important;
- position: relative;
-}
-[class^=ais-] [class^=ais-] [class^=ais-] [class^=ais-]:hover:after {
- background: cyan;
- top: 40px;
- z-index: 1100;
-}
diff --git a/src/css/default/_breadcrumb.scss b/src/css/default/_breadcrumb.scss
deleted file mode 100644
index 0086c24f57..0000000000
--- a/src/css/default/_breadcrumb.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.ais-breadcrumb--label,
-.ais-breadcrumb--separator,
-.ais-breadcrumb--home {
- display: inline;
- color: #3369E7;
-}
-
-.ais-breadcrumb--item {
- display: inline;
-}
-
-.ais-breadcrumb--disabledLabel {
- color: rgb(68, 68, 68);
- display: inline;
-}
diff --git a/src/css/default/_clear-all.scss b/src/css/default/_clear-all.scss
deleted file mode 100644
index 00f89a2fed..0000000000
--- a/src/css/default/_clear-all.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(clear-all) {
- @include element(header) {
- /* widget header */
- }
-
- @include element(body) {
- /* widget body */
- }
-
- @include element(link) {
- /* widget link */
- }
-
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_current-refined-values.scss b/src/css/default/_current-refined-values.scss
deleted file mode 100644
index a9fb58f4bf..0000000000
--- a/src/css/default/_current-refined-values.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(current-refined-values) {
- @include element(header) {
- /* widget header */
- }
-
- @include element(body) {
- /* widget body */
- }
-
- @include element(clear-all) {
- /* widget clearAll link */
- }
-
- @include element(list) {
- /* widget list */
- }
-
- @include element(item) {
- /* widget item */
- }
-
- @include element(link) {
- /* widget link */
- }
-
- @include element(count) {
- /* widget count */
- }
-
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_geo-search.scss b/src/css/default/_geo-search.scss
deleted file mode 100644
index 89cb870ee1..0000000000
--- a/src/css/default/_geo-search.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(geo-search) {
- /* root element */
- height: 100%;
-
- @include element(map) {
- /* map element */
- height: 100%;
- }
- @include element(controls) {
- /* map controls */
- }
- @include element(clear) {
- /* clear button */
- }
- @include element(control) {
- /* refine control */
- }
- @include element(toggle-label) {
- /* toggle label */
- display: flex;
- align-items: center;
- }
- @include element(toggle-input) {
- /* toggle input */
- }
- @include element(redo) {
- /* redo button */
- }
-}
diff --git a/src/css/default/_header-footer.scss b/src/css/default/_header-footer.scss
deleted file mode 100644
index 4e54a2c072..0000000000
--- a/src/css/default/_header-footer.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.ais-root__collapsible .ais-header {
- cursor: pointer;
-}
-
-.ais-root__collapsed .ais-body, .ais-root__collapsed .ais-footer {
- display: none;
-}
diff --git a/src/css/default/_hierarchical-menu.scss b/src/css/default/_hierarchical-menu.scss
deleted file mode 100644
index bc53110bc0..0000000000
--- a/src/css/default/_hierarchical-menu.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(hierarchical-menu) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* widget body */
- }
- @include element(list) {
- /* item list */
- @include modifier(lvl0) {
- /* item list level 0 */
- }
- @include modifier(lvl1) {
- /* item list level 1 */
- margin-left: 10px;
- }
- @include modifier(lvl2) {
- /* item list level 0 */
- margin-left: 10px;
- }
- }
-
- @include element(item) {
- /* list item */
- @include modifier(active) {
- /* active list item */
- }
- }
-
- @include element(link) {
- /* item link */
- }
- @include element(count) {
- /* item count */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_hits.scss b/src/css/default/_hits.scss
deleted file mode 100644
index a6a02dd9f3..0000000000
--- a/src/css/default/_hits.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(hits) {
- @include modifier(empty) {
- /* empty container */
- }
- @include element(item) {
- /* hit item */
- }
-}
diff --git a/src/css/default/_menu.scss b/src/css/default/_menu.scss
deleted file mode 100644
index 2cec0b7a0d..0000000000
--- a/src/css/default/_menu.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(menu) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* widget body */
- }
- @include element(list) {
- /* item list */
- }
- @include element(item) {
- /* list item */
- @include modifier(active) {
- /* active list item */
- }
- }
- @include element(link) {
- /* item link */
- }
- @include element(count) {
- /* item count */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_pagination.scss b/src/css/default/_pagination.scss
deleted file mode 100644
index 0c2bcd2a7a..0000000000
--- a/src/css/default/_pagination.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(pagination) {
- @include element(item) {
- /* pagination item */
- display: inline-block;
- padding: 3px;
- @include modifier(disabled) {
- /* disabled pagination item */
- visibility: hidden;
- }
- @include modifier(active) {
- /* active pagination item */
- }
- @include modifier(first) {
- /* first pagination item */
- }
- @include modifier(previous) {
- /* previous pagination item */
- }
- @include modifier(page) {
- /* page pagination item */
- }
- @include modifier(next) {
- /* next pagination item */
- }
- @include modifier(last) {
- /* last pagination item */
- }
- }
- @include element(link) {
- /* pagination link */
- }
-}
diff --git a/src/css/default/_price-ranges.scss b/src/css/default/_price-ranges.scss
deleted file mode 100644
index c0e7373f83..0000000000
--- a/src/css/default/_price-ranges.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(price-ranges) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* widget body */
- }
- @include element(footer) {
- /* widget footer */
- }
- @include element(list) {
- /* item list */
- }
- @include element(item) {
- /* list item */
- @include modifier(active) {
- /* active list item */
- }
- }
-
- @include element(link) {
- /* item link */
- }
- @include element(form) {
- /* custom form */
- }
- @include element(label) {
- /* custom form label */
- }
- @include element(currency) {
- /* currency */
- }
- @include element(input) {
- /* custom form input */
- }
- @include element(separator) {
- /* custom form separator */
- }
- @include element(button) {
- /* custom form button */
- }
-}
-
-
diff --git a/src/css/default/_range-input.scss b/src/css/default/_range-input.scss
deleted file mode 100644
index 7d2922ee2a..0000000000
--- a/src/css/default/_range-input.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@import "../base";
-@import "variables";
-
-$input-min-width: 165px;
-
-@include block(range-input) {
- @include element(fieldset) {
- /* custom fieldset */
- margin: 0;
- padding: 0;
- border: 0;
- }
- @include element(labelMin) {
- /* custom label min */
- display: inline-block;
- }
- @include element(inputMin) {
- /* custom input min */
- min-width: $input-min-width;
-
- &:hover:disabled {
- cursor: not-allowed;
- }
- }
- @include element(separator) {
- /* separator */
- margin: 0 5px;
- }
- @include element(labelMax) {
- /* custom label max */
- display: inline-block;
- }
- @include element(inputMax) {
- /* custom input max */
- min-width: $input-min-width;
-
- &:hover:disabled {
- cursor: not-allowed;
- }
- }
- @include element(submit) {
- /* custom form button */
- margin-left: 5px;
-
- &:disabled,
- &:hover:disabled {
- cursor: not-allowed;
- background-color: #C9C9C9;
- }
- }
-}
diff --git a/src/css/default/_range-slider.scss b/src/css/default/_range-slider.scss
deleted file mode 100644
index ae014cdc49..0000000000
--- a/src/css/default/_range-slider.scss
+++ /dev/null
@@ -1,113 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-$range-slider-handle-size: 20px;
-$range-slider-target-height: 6px;
-$range-slider-bg: $gray-light;
-$range-slider-marker-bg: #DDD;
-$range-slider-bar-color: $blue-light;
-$range-slider-handle-bg: $white;
-
-@include block(range-slider) {
- .ais-range-slider--disabled {
- cursor: not-allowed;
-
- .ais-range-slider--handle {
- border-color: $range-slider-marker-bg;
- cursor: not-allowed;
- }
-
- .rheostat-horizontal .rheostat-progress {
- background-color: $range-slider-marker-bg;
- }
- }
-
- .rheostat {
- overflow: visible;
- margin-top: 2em;
- margin-bottom: 2em;
- }
-
- .rheostat-background {
- background-color: $range-slider-handle-bg;
- border-top: 1px solid $range-slider-marker-bg;
- border-bottom: 1px solid $range-slider-marker-bg;
- border-left: 2px solid $range-slider-marker-bg;
- border-right: 2px solid $range-slider-marker-bg;
- position: relative;
- }
-
- .rheostat-horizontal {
- .rheostat-background {
- height: 6px;
- top: 0;
- width: 100%;
- }
-
- .rheostat-progress {
- background-color: $range-slider-bar-color;
- position: absolute;
- height: 4px;
- top: 1px;
- }
-
- .rheostat-handle {
- margin-left: -12px;
- top: -7px;
-
- .ais-range-slider--tooltip {
- text-align: center;
- margin-left: -10px;
- width: 40px;
- }
- }
-
- .rheostat-handle {
- &::before,
- &::after {
- top: 7px;
- height: 10px;
- width: 1px;
- }
-
- &::before { left: 10px; }
- &::after { left: 13px; }
- }
- }
-
- @include element(handle) {
- width: $range-slider-handle-size;
- height: $range-slider-handle-size;
- position: relative;
- z-index: 1;
- background: $range-slider-handle-bg;
- border: 1px solid $range-slider-bar-color;
- border-radius: 50%;
- cursor: pointer;
- }
-
- @include element(tooltip) {
- position: absolute;
- background: $white;
- top: -$range-slider-handle-size - 2px;
- font-size: .8em;
- }
-
- @include element(value) {
- width: $range-slider-handle-size * 2;
- position: absolute;
- text-align: center;
- margin-left: -$range-slider-handle-size;
- padding-top: 15px;
- font-size: .8em;
- }
-
- @include element(marker) {
- position: absolute;
- background: $range-slider-marker-bg;
- margin-left: -1px;
- width: 1px;
- height: 5px;
- }
-}
diff --git a/src/css/default/_refinement-list.scss b/src/css/default/_refinement-list.scss
deleted file mode 100644
index 3c603b4330..0000000000
--- a/src/css/default/_refinement-list.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(refinement-list) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* wudget footer */
- }
- @include element(list) {
- /* item list */
- }
- @include element(item) {
- /* list item */
- @include modifier(active) {
- /* active list item */
- }
- }
- @include element(label) {
- /* item label */
- }
- @include element(checkbox) {
- /* item checkbox */
- }
- @include element(count) {
- /* item count */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
-
-/* Sub block for the show more of the refinement list */
-@include block(show-more) {
- @include modifier(active) {
- /* Show more button is activated */
- }
- @include modifier(inactive) {
- /* Show more button is deactivated */
- }
-}
diff --git a/src/css/default/_refinement-searchbox.scss b/src/css/default/_refinement-searchbox.scss
deleted file mode 100644
index 575318cc71..0000000000
--- a/src/css/default/_refinement-searchbox.scss
+++ /dev/null
@@ -1,219 +0,0 @@
-// sass-lint:disable no-vendor-prefixes
-$config-sffv: (
- input-width: 100%,
- input-height: 25px,
- border-width: 1px,
- border-radius: 3px,
- input-border-color: #CCCCCC,
- input-focus-border-color: #337AB7,
- input-background: #FFFFFF,
- input-focus-background: #FFFFFF,
- font-size: 14px,
- placeholder-color: #BBBBBB,
- icon-size: 14px,
- icon-position: left,
- icon-color: #337AB7,
- icon-background: #FFFFFF,
- icon-background-opacity: 0,
- icon-clear-size: 14px
-);
-
-
-@function even-px($value) {
- @if type-of($value) == 'number' {
- @if (unitless($value)) {
- $value: $value * 1px;
- } @else if unit($value) == 'em' {
- $value: ($value / 1em * 16px);
- } @else if unit($value) == 'pts' {
- $value: $value * 1.3333 * 1px;
- } @else if unit($value) == '%' {
- $value: $value * 16 / 100% * 1px;
- };
- $value: round($value);
- @if ($value % 2 != 0) {
- $value: $value + 1;
- }
- @return $value;
- }
-}
-
-@mixin searchbox(
- $font-size: 90%,
- $input-width: 350px,
- $input-height: $font-size * 2.4,
- $border-width: 1px,
- $border-radius: $input-height / 2,
- $input-border-color: #CCC,
- $input-focus-border-color: #1EC9EA,
- $input-background: #F8F8F8,
- $input-focus-background: #FFF,
- $placeholder-color: #AAA,
- $icon: 'sbx-icon-search-1',
- $icon-size: $input-height / 1.6,
- $icon-position: left,
- $icon-color: #888,
- $icon-background: $input-focus-border-color,
- $icon-background-opacity: .1,
- $icon-clear: 'sbx-icon-clear-1',
- $icon-clear-size: $font-size / 1.1
-) {
- display: inline-block;
- position: relative;
- width: $input-width;
- height: even-px($input-height);
- white-space: nowrap;
- box-sizing: border-box;
- font-size: $font-size;
-
- &__wrapper {
- width: 100%;
- height: 100%;
- }
-
- &__input {
- display: inline-block;
- transition: box-shadow .4s ease, background .4s ease;
- border: 0;
- border-radius: even-px($border-radius);
- box-shadow: inset 0 0 0 $border-width $input-border-color;
- background: $input-background;
- padding: 0;
- padding-right: if($icon-position == 'right', even-px($input-height) + even-px($icon-clear-size) + 8px, even-px($input-height * .8)) + if($icon-background-opacity == 0, 0, even-px($font-size));
- padding-left: if($icon-position == 'right', even-px($font-size / 2) + even-px($border-radius / 2), even-px($input-height) + if($icon-background-opacity == 0, 0, even-px($font-size * 1.2)));
- width: 100%;
- height: 100%;
- vertical-align: middle;
- white-space: normal;
- font-size: inherit;
- appearance: none;
-
- &::-webkit-search-decoration,
- &::-webkit-search-cancel-button,
- &::-webkit-search-results-button,
- &::-webkit-search-results-decoration {
- display: none;
- }
-
- &:hover {
- box-shadow: inset 0 0 0 $border-width darken($input-border-color, 10%);
- }
-
- &:focus,
- &:active {
- outline: 0;
- box-shadow: inset 0 0 0 $border-width $input-focus-border-color;
- background: $input-focus-background;
- }
-
- &::placeholder {
- color: $placeholder-color;
- }
-
- }
-
- &__submit {
- position: absolute;
- top: 0;
- @if $icon-position == 'right' {
- right: 0;
- left: inherit;
- } @else {
- right: inherit;
- left: 0;
- }
- margin: 0;
- border: 0;
- border-radius: if($icon-position == 'right', 0 $border-radius $border-radius 0, $border-radius 0 0 $border-radius);
- background-color: rgba($icon-background, $icon-background-opacity);
- padding: 0;
- width: even-px($input-height) + if($icon-background-opacity == 0, 0, even-px($font-size / 2));
- height: 100%;
- vertical-align: middle;
- text-align: center;
- font-size: inherit;
- user-select: none;
-
- // Helper for vertical alignement of the icon
- &::before {
- display: inline-block;
- margin-right: -4px;
- height: 100%;
- vertical-align: middle;
- content: '';
- }
-
- &:hover,
- &:active {
- cursor: pointer;
- }
-
- &:focus {
- outline: 0;
- }
-
- svg {
- width: even-px($icon-size);
- height: even-px($icon-size);
- vertical-align: middle;
- fill: $icon-color;
- }
- }
-
- &__reset {
- display: none;
- position: absolute;
- top: (even-px($input-height) - even-px($icon-clear-size)) / 2 - 4px;
- right: if($icon-position == 'right',
- even-px($input-height) + if($icon-background-opacity == 0, 0 , even-px($font-size)),
- (even-px($input-height) - even-px($icon-clear-size)) / 2 - 4px);
- margin: 0;
- border: 0;
- background: none;
- cursor: pointer;
- padding: 0;
- font-size: inherit;
- user-select: none;
- fill: rgba(#000, .5);
-
- &:focus {
- outline: 0;
- }
-
- svg {
- display: block;
- margin: 4px;
- width: even-px($icon-clear-size);
- height: even-px($icon-clear-size);
- }
- }
-
- &__input:valid ~ &__reset { // sass-lint:disable-line force-pseudo-nesting
- display: block;
- animation-name: sbx-reset-in;
- animation-duration: .15s;
- }
-
- @at-root {
- @keyframes sbx-reset-in {
- 0% {
- transform: translate3d(-20%, 0, 0);
- opacity: 0;
- }
-
- 100% {
- transform: none;
- opacity: 1;
- }
- }
- }
-}
-
-.sbx-sffv {
- @include searchbox($config-sffv...);
-}
-
-.ais-refinement-list--item em {
- font-style: normal;
- font-weight: bold;
-}
diff --git a/src/css/default/_search-box.scss b/src/css/default/_search-box.scss
deleted file mode 100644
index d7f6caedee..0000000000
--- a/src/css/default/_search-box.scss
+++ /dev/null
@@ -1,97 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(search-box) {
- position: relative;
- max-width: 300px;
- width: 100%;
-
- @include element(input) {
- /* search input */
- padding-left: 24px;
- height: 100%;
- width: 100%;
- }
-
- @include element(magnifier) {
- background: transparent;
- position: absolute;
- user-select: none;
- top: 4px;
- left: 7px;
-
- svg {
- display: block;
- vertical-align: middle;
- height: 14px;
- width: 14px;
- }
- }
-
- @include element(loading-indicator-wrapper) {
- display: none;
- background: transparent;
- position: absolute;
- user-select: none;
- top: 4px;
- left: 7px;
-
- svg {
- vertical-align: middle;
- height: 14px;
- width: 14px;
- }
- }
-
- @include element(reset) {
- background: none;
- cursor: pointer;
- position: absolute;
- top: 5px;
- right: 5px;
- margin: 0;
- border: 0;
- padding: 0;
- user-select: none;
-
- svg {
- display: block;
- width: 12px;
- height: 12px;
- }
- }
-
- @include element(powered-by) {
- font-size: .8em;
- text-align: right;
- margin-top: 2px;
- }
- //
- // Image replacement technique used:
- // http://www.zeldman.com/2012/03/01/replacing-the-9999px-hack-new-image-replacement/
- //
- @include element(powered-by-link) {
- display: inline-block;
- width: 45px;
- height: 16px;
- text-indent: 101%;
- overflow: hidden;
- white-space: nowrap;
- background-image: url('data:image/svg+xml;utf8, ');
- background-repeat: no-repeat;
- background-size: contain;
- vertical-align: middle;
- }
-}
-
-.ais-search-box.ais-stalled-search {
- .ais-search-box--magnifier-wrapper {
- display: none;
- }
-
- .ais-search-box--loading-indicator-wrapper {
- display: block;
- }
-}
-
diff --git a/src/css/default/_sort-by-selector.scss b/src/css/default/_sort-by-selector.scss
deleted file mode 100644
index 37d512fafd..0000000000
--- a/src/css/default/_sort-by-selector.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(sort-by-selector) {
- @include element(item) {
- /* selector item */
- }
-}
diff --git a/src/css/default/_star-rating.scss b/src/css/default/_star-rating.scss
deleted file mode 100644
index a045ff0557..0000000000
--- a/src/css/default/_star-rating.scss
+++ /dev/null
@@ -1,72 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-$stars-color: #FBAE00;
-$stars-color-disabled: #C9C9C9;
-
-@include block(star-rating) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* wudget footer */
- }
- @include element(list) {
- /* item list */
- }
- @include element(item) {
- /* list item */
- vertical-align: middle;
-
- @include modifier(active) {
- /* active list item */
- font-weight: bold;
- }
- }
- @include element(star) {
- /* item star */
- display: inline-block;
- width: 1em;
- height: 1em;
-
- &::before {
- content: '\2605';
- color: $stars-color;
- }
-
- @include modifier(empty) {
- /* empty star */
- display: inline-block;
- width: 1em;
- height: 1em;
-
- &::before {
- content: '\2606';
- color: $stars-color;
- }
- }
- }
- @include element(link) {
- /* item link */
- @include modifier(disabled) {
- /* disabled list item */
- @include bem(star-rating, star) {
- &::before {
- color: $stars-color-disabled;
- }
- }
- @include bem(star-rating, star, empty) {
- &::before {
- color: $stars-color-disabled;
- }
- }
- }
- }
- @include element(count) {
- /* item count */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_stats.scss b/src/css/default/_stats.scss
deleted file mode 100644
index 6b52baf24c..0000000000
--- a/src/css/default/_stats.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(stats) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* widget body */
- }
- @include element(time) {
- /* processing time */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_toggle.scss b/src/css/default/_toggle.scss
deleted file mode 100644
index 8e63c8edcf..0000000000
--- a/src/css/default/_toggle.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-// sass-lint:disable no-empty-rulesets
-@import "../base";
-@import "variables";
-
-@include block(toggle) {
- @include element(header) {
- /* widget header */
- }
- @include element(body) {
- /* wudget body */
- }
- @include element(list) {
- /* item list */
- }
- @include element(item) {
- /* list item */
- @include modifier(active) {
- /* active list item */
- }
- }
- @include element(label) {
- /* item label */
- }
- @include element(checkbox) {
- /* item checkbox */
- }
- @include element(count) {
- /* item count */
- }
- @include element(footer) {
- /* widget footer */
- }
-}
diff --git a/src/css/default/_variables.scss b/src/css/default/_variables.scss
deleted file mode 100644
index ae579079bc..0000000000
--- a/src/css/default/_variables.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-$white: #FFFFFF;
-$gray-light: #F3F4F7;
-$blue-dark: #1F3B5D;
-$blue-light: #46AEDA;
diff --git a/src/css/instantsearch-theme-algolia.scss b/src/css/instantsearch-theme-algolia.scss
deleted file mode 100644
index a2e7936be0..0000000000
--- a/src/css/instantsearch-theme-algolia.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import 'theme/base';
diff --git a/src/css/instantsearch.scss b/src/css/instantsearch.scss
deleted file mode 100644
index e736661753..0000000000
--- a/src/css/instantsearch.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@import "base";
-@import "default/search-box";
-@import "default/refinement-searchbox";
-@import "default/stats";
-@import "default/sort-by-selector";
-@import "default/hits";
-@import "default/pagination";
-@import "default/refinement-list";
-@import "default/menu";
-@import "default/toggle";
-@import "default/hierarchical-menu";
-@import "default/range-input";
-@import "default/range-slider";
-@import "default/star-rating";
-@import "default/price-ranges";
-@import "default/clear-all";
-@import "default/current-refined-values";
-@import "default/header-footer";
-@import "default/breadcrumb";
-@import "default/geo-search";
diff --git a/src/css/theme/_base.scss b/src/css/theme/_base.scss
deleted file mode 100644
index c0486b070d..0000000000
--- a/src/css/theme/_base.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-@import 'variables';
-@import 'clear-all';
-@import 'current-refined-values';
-@import 'hierarchical-menu';
-@import 'hits-per-page';
-@import 'menu';
-@import 'refinement-list';
-@import 'numeric-selector';
-@import 'pagination';
-@import 'price-ranges';
-@import 'range-input';
-@import 'range-slider';
-@import 'search-box';
-@import 'sort-by-selector';
-@import 'star-rating';
-@import 'stats';
-@import 'toggle';
-@import 'menu-select';
-@import 'breadcrumb';
-@import 'geo-search';
-
-[class^="ais-"] {
- box-sizing: border-box;
-
- & > *,
- & > *::after,
- & > *::before {
- box-sizing: border-box;
- }
-}
-
-.ais-header {
- border-bottom: 2px solid #EEE;
- font-size: .8em;
- margin: 0 0 6px;
- padding: 0 0 6px;
- text-transform: uppercase;
-}
diff --git a/src/css/theme/_breadcrumb.scss b/src/css/theme/_breadcrumb.scss
deleted file mode 100644
index 2db159f827..0000000000
--- a/src/css/theme/_breadcrumb.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-.ais-breadcrumb--root {
- .ais-breadcrumb--label,
- .ais-breadcrumb--separator,
- .ais-breadcrumb--home {
- display: inline;
- color: #3369E7;
-
- div {
- display: inline;
- }
- }
-
- .ais-breadcrumb--disabledLabel {
- color: rgb(68, 68, 68);
- display: inline;
- }
-
- .ais-breadcrumb--separator {
- position: relative;
- display: inline-block;
- height: 14px;
- width: 14px;
- &::after {
- background: url("data:image/svg+xml;utf8, ") no-repeat center center / contain;
- content: ' ';
- display: block;
- position: absolute;
- top: 2px;
- height: 14px;
- width: 14px;
- }
- }
-}
diff --git a/src/css/theme/_clear-all.scss b/src/css/theme/_clear-all.scss
deleted file mode 100644
index d9f0558e87..0000000000
--- a/src/css/theme/_clear-all.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-.ais-clear-all {
- &--link {
- color: $white;
- display: inline-block;
- background: $blue;
- border-radius: $border-radius;
- font-size: $font-size;
- text-decoration: none;
- padding: 4px 8px;
-
- &:hover {
- text-decoration: none;
- color: $white;
- background: $dark-blue;
- }
- }
-
- &--link-disabled {
- opacity: .5;
- pointer-events: none;
- }
-}
diff --git a/src/css/theme/_current-refined-values.scss b/src/css/theme/_current-refined-values.scss
deleted file mode 100644
index a0baed30b0..0000000000
--- a/src/css/theme/_current-refined-values.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-.ais-current-refined-values {
- &--clear-all {
- @extend .ais-clear-all--link;
- margin-bottom: 5px;
- }
-
- &--clear-all-disabled {
- @extend .ais-clear-all--link-disabled;
- }
-
- &--item {
- font-size: $font-size + 2px;
- line-height: $font-size * 2.5;
- }
-
- &--link {
- color: $light-blue;
- text-decoration: none;
-
- &:hover {
- color: $dark-blue;
- text-decoration: none;
- }
- }
-
- &--count {
- background: rgba(39, 81, 175, 0.1);
- border-radius: 31px;
- color: $light-blue;
- padding: 2px 10px;
- }
-}
diff --git a/src/css/theme/_geo-search.scss b/src/css/theme/_geo-search.scss
deleted file mode 100644
index 7bcfdcc5a9..0000000000
--- a/src/css/theme/_geo-search.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-.ais-geo-search {
- position: relative;
-}
-
-.ais-geo-search--clear {
- @extend .ais-clear-all--link;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- padding: 8px 15px;
- position: absolute;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
-
- &:hover {
- cursor: pointer;
- }
-}
-
-.ais-geo-search--control {
- position: absolute;
- top: 10px;
- left: 50px;
-}
-
-.ais-geo-search--toggle-label {
- @extend .ais-current-refined-values--item;
- @extend .ais-refinement-list--label;
- font-size: $font-size;
- background: #FFFFFF;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- padding: 0 15px;
-}
-
-.ais-geo-search--redo {
- @extend .ais-clear-all--link;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- padding: 8px 15px;
-
- &:hover {
- cursor: pointer;
- }
-
- &:disabled {
- background: #A0B8F3;
- }
-}
diff --git a/src/css/theme/_hierarchical-menu.scss b/src/css/theme/_hierarchical-menu.scss
deleted file mode 100644
index fe417412f3..0000000000
--- a/src/css/theme/_hierarchical-menu.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-.ais-hierarchical-menu {
- &--item {
- @extend .ais-current-refined-values--item;
-
- &__active > div > .ais-hierarchical-menu--link {
- font-weight: bold;
-
- &::after {
- transform: rotate(90deg);
- }
- }
- }
-
- &--link {
- @extend .ais-current-refined-values--link;
- position: relative;
-
- &::after {
- background: url("data:image/svg+xml;utf8, ") no-repeat center center/contain;
- content: ' ';
- display: block;
- position: absolute;
- top: calc(50% - 14px / 2);
- right: -22px;
- height: 14px;
- width: 14px;
- }
- }
-
- &--count {
- @extend .ais-current-refined-values--count;
- }
-}
diff --git a/src/css/theme/_hits-per-page.scss b/src/css/theme/_hits-per-page.scss
deleted file mode 100644
index 202a64cff0..0000000000
--- a/src/css/theme/_hits-per-page.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-select.ais-hits-per-page-selector { // sass-lint:disable-line no-qualifying-elements
- appearance: none;
-
- background: $white url("data:image/svg+xml;utf8, ") no-repeat center right 16px/10px;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2) !important;
- border: solid 1px #D4D8E3 !important;
- border-radius: $border-radius;
- color: $grey;
- font-size: $font-size;
- transition: background 0.2s ease, box-shadow 0.2s ease;
- padding: 8px 32px 8px 16px;
- outline: none;
-}
diff --git a/src/css/theme/_menu-select.scss b/src/css/theme/_menu-select.scss
deleted file mode 100644
index abfb9e7dde..0000000000
--- a/src/css/theme/_menu-select.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-select.ais-menu-select--select { // sass-lint:disable-line no-qualifying-elements
- @extend select.ais-hits-per-page-selector; // sass-lint:disable-line no-qualifying-elements
-}
diff --git a/src/css/theme/_menu.scss b/src/css/theme/_menu.scss
deleted file mode 100644
index d87cb58352..0000000000
--- a/src/css/theme/_menu.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.ais-menu {
- &--item {
- @extend .ais-current-refined-values--item;
-
- &__active > div > .ais-menu--link {
- font-weight: bold;
- }
- }
-
- &--link {
- @extend .ais-current-refined-values--link;
- }
-
- &--count {
- @extend .ais-current-refined-values--count;
- }
-
- button {
- background: transparent;
- border: 0;
- cursor: pointer;
- font-size: $font-size - 1px;
- }
-}
diff --git a/src/css/theme/_numeric-selector.scss b/src/css/theme/_numeric-selector.scss
deleted file mode 100644
index 48dfbbba3c..0000000000
--- a/src/css/theme/_numeric-selector.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-select.ais-numeric-selector { // sass-lint:disable-line no-qualifying-elements
- @extend select.ais-hits-per-page-selector; // sass-lint:disable-line no-qualifying-elements
-}
diff --git a/src/css/theme/_pagination.scss b/src/css/theme/_pagination.scss
deleted file mode 100644
index 87e0ec012e..0000000000
--- a/src/css/theme/_pagination.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-.ais-pagination {
- background: $white;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- display: inline-block;
- padding: 8px 16px;
- width: auto;
-
- &--item {
- border-radius: $border-radius;
- font-size: $font-size + 2px;
- text-align: center;
- width: 28px;
-
- &:hover {
- background: rgba(39, 81, 175, .1);
- }
-
- &__disabled {
- color: #BBB;
- opacity: .5;
- pointer-events: none;
- visibility: visible;
- }
-
- &__active {
- background: $blue;
-
- .ais-pagination--link {
- color: $white;
- }
- }
- }
-
- &--link {
- color: $grey;
- display: block;
- text-decoration: none;
- width: 100%;
-
- &:hover {
- color: $blue;
- text-decoration: none;
- }
- }
-}
diff --git a/src/css/theme/_price-ranges.scss b/src/css/theme/_price-ranges.scss
deleted file mode 100644
index 94e079440d..0000000000
--- a/src/css/theme/_price-ranges.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-.ais-price-ranges {
- &--item {
- font-size: $font-size + 2px;
- line-height: $font-size * 2;
-
- &__active {
- font-weight: bold;
- }
- }
-
- &--link {
- @extend .ais-current-refined-values--link;
- }
-
- &--form {
- margin-top: 10px;
- }
-
- &--input {
- background: $white;
- box-shadow: inset 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- outline: none;
- }
-
- &--button {
- @extend .ais-clear-all--link;
- border: 0;
- outline: none;
- margin-left: 5px;
- position: relative;
- top: -2px;
- }
-}
diff --git a/src/css/theme/_range-input.scss b/src/css/theme/_range-input.scss
deleted file mode 100644
index f76e433adb..0000000000
--- a/src/css/theme/_range-input.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-.ais-range-input {
- &--inputMin,
- &--inputMax {
- background: $white;
- box-shadow: inset 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- border: solid 1px #D4D8E3;
- border-radius: $border-radius;
- outline: none;
- }
-
- &--submit {
- @extend .ais-clear-all--link;
- border: none;
- outline: none;
- margin-left: 5px;
- position: relative;
- top: -2px;
- }
-}
diff --git a/src/css/theme/_range-slider.scss b/src/css/theme/_range-slider.scss
deleted file mode 100644
index b2150d8950..0000000000
--- a/src/css/theme/_range-slider.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.ais-range-slider {
- &--handle {
- border: 1px solid $blue;
- }
-
- .rheostat-progress {
- background-color: $blue !important;
- }
-}
diff --git a/src/css/theme/_refinement-list.scss b/src/css/theme/_refinement-list.scss
deleted file mode 100644
index dfb7af98e7..0000000000
--- a/src/css/theme/_refinement-list.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-.ais-refinement-list {
- &--item {
- @extend .ais-current-refined-values--item;
- line-height: $font-size * 2;
-
- &__active > div > .ais-refinement-list--label {
- font-weight: bold;
- }
- }
-
- &--label {
- @extend .ais-current-refined-values--link;
- cursor: pointer;
-
- input[type="radio"], // sass-lint:disable-line no-qualifying-elements force-attribute-nesting
- input[type="checkbox"] { // sass-lint:disable-line no-qualifying-elements force-attribute-nesting
- margin-right: 5px;
- }
- }
-
- &--count {
- @extend .ais-current-refined-values--count;
- }
-
- div > button {
- background: transparent;
- border: 0;
- cursor: pointer;
- font-size: $font-size - 1px;
- }
-}
diff --git a/src/css/theme/_search-box.scss b/src/css/theme/_search-box.scss
deleted file mode 100644
index 68d41953fe..0000000000
--- a/src/css/theme/_search-box.scss
+++ /dev/null
@@ -1,58 +0,0 @@
-.ais-search-box {
- display: inline-block;
- position: relative;
- height: 46px;
- white-space: nowrap;
- font-size: 14px;
-
-
- &--input {
- appearance: none;
- font: inherit;
- background: $white;
- color: $black;
- display: inline-block;
- border: 1px solid #D4D8E3;
- border-radius: $border-radius;
- box-shadow: 0 1px 1px 0 rgba(85, 95, 110, 0.2);
- transition: box-shadow .4s ease, background .4s ease;
- padding: 10px 10px 10px 35px;
- vertical-align: middle;
- white-space: normal;
- height: 100%;
- width: 100%;
-
- &:focus {
- box-shadow: none;
- outline: 0;
- }
- }
-
- &--reset {
- fill: #BFC7D8;
- top: calc(50% - 12px / 2);
- right: 13px;
- }
-
- &--magnifier {
- fill: #BFC7D8;
- left: 12px;
- top: calc(50% - 18px / 2);
-
- svg {
- height: 18px;
- width: 18px;
- }
- }
-
- &--loading-indicator-wrapper {
- fill: #BFC7D8;
- left: 12px;
- top: calc(50% - 18px / 2);
-
- svg {
- height: 18px;
- width: 18px;
- }
- }
-}
diff --git a/src/css/theme/_sort-by-selector.scss b/src/css/theme/_sort-by-selector.scss
deleted file mode 100644
index 66ca5344b1..0000000000
--- a/src/css/theme/_sort-by-selector.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-select.ais-sort-by-selector { // sass-lint:disable-line no-qualifying-elements
- @extend select.ais-hits-per-page-selector; // sass-lint:disable-line no-qualifying-elements
-}
diff --git a/src/css/theme/_star-rating.scss b/src/css/theme/_star-rating.scss
deleted file mode 100644
index 9f61aba85d..0000000000
--- a/src/css/theme/_star-rating.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.ais-star-rating {
- &--item {
- @extend .ais-current-refined-values--item;
- }
-
- &--link {
- @extend .ais-current-refined-values--link;
- }
-
- &--count {
- @extend .ais-current-refined-values--count;
- }
-}
diff --git a/src/css/theme/_stats.scss b/src/css/theme/_stats.scss
deleted file mode 100644
index 83ddc1073c..0000000000
--- a/src/css/theme/_stats.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.ais-stats {
- color: $grey;
- font-size: $font-size + 2px;
- opacity: .6;
-}
diff --git a/src/css/theme/_toggle.scss b/src/css/theme/_toggle.scss
deleted file mode 100644
index d5af18b404..0000000000
--- a/src/css/theme/_toggle.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-.ais-toggle {
- &--item {
- @extend .ais-current-refined-values--item;
-
- &__active {
- font-weight: bold;
- }
- }
-
- &--label {
- @extend .ais-refinement-list--label;
- }
-
- &--count {
- @extend .ais-current-refined-values--count;
- }
-}
diff --git a/src/css/theme/_variables.scss b/src/css/theme/_variables.scss
deleted file mode 100644
index 6156d5b261..0000000000
--- a/src/css/theme/_variables.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-// +--------+
-// | LAYOUT |
-// +--------+
-$border-radius: 4px;
-$font-size: 12px;
-
-// +--------+
-// | COLORS |
-// +--------+
-$white: #FFFFFF;
-$black: #000000;
-$light-blue: #3E82F7;
-$blue: #3369E7;
-$dark-blue: #184ECD;
-$grey: #697782;
diff --git a/src/decorators/__tests__/__snapshots__/autoHideContainer-test.js.snap b/src/decorators/__tests__/__snapshots__/autoHideContainer-test.js.snap
deleted file mode 100644
index 5c80165103..0000000000
--- a/src/decorators/__tests__/__snapshots__/autoHideContainer-test.js.snap
+++ /dev/null
@@ -1,16 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`autoHideContainer should render autoHideContainer( ) 1`] = `
-
-
-
-`;
diff --git a/src/decorators/__tests__/__snapshots__/headerFooter-test.js.snap b/src/decorators/__tests__/__snapshots__/headerFooter-test.js.snap
deleted file mode 100644
index c9d5b78383..0000000000
--- a/src/decorators/__tests__/__snapshots__/headerFooter-test.js.snap
+++ /dev/null
@@ -1,230 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`headerFooter collapsible when collapsed 1`] = `
-
-`;
-
-exports[`headerFooter collapsible when true 1`] = `
-
-`;
-
-exports[`headerFooter should add a footer if such a template is passed 1`] = `
-
-`;
-
-exports[`headerFooter should add a header if such a template is passed 1`] = `
-
-`;
-
-exports[`headerFooter should render the component in a root and body 1`] = `
-
-`;
diff --git a/src/decorators/__tests__/autoHideContainer-test.js b/src/decorators/__tests__/autoHideContainer-test.js
deleted file mode 100644
index 90ffa8e1cd..0000000000
--- a/src/decorators/__tests__/autoHideContainer-test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import ReactDOM from 'react-dom';
-import { shallow } from 'enzyme';
-import autoHideContainer from '../autoHideContainer';
-
-class TestComponent extends Component {
- render() {
- return {this.props.hello}
;
- }
-}
-
-TestComponent.propTypes = {
- hello: PropTypes.string,
-};
-
-describe('autoHideContainer', () => {
- let props = {};
-
- it('should render autoHideContainer( )', () => {
- props.hello = 'son';
- const AutoHide = autoHideContainer(TestComponent);
- const out = shallow( );
- expect(out).toMatchSnapshot();
- });
-
- describe('props.shouldAutoHideContainer', () => {
- let AutoHide;
- let component;
- let container;
- let innerContainer;
-
- beforeEach(() => {
- AutoHide = autoHideContainer(TestComponent);
- container = document.createElement('div');
- props = { hello: 'mom', shouldAutoHideContainer: false };
- component = ReactDOM.render( , container);
- });
-
- it('creates a component', () => {
- expect(component).toBeDefined();
- });
-
- it('shows the container at first', () => {
- expect(container.style.display).not.toEqual('none');
- });
-
- describe('when set to true', () => {
- beforeEach(() => {
- jest.spyOn(component, 'render');
- props.shouldAutoHideContainer = true;
- ReactDOM.render( , container);
- innerContainer = container.firstElementChild;
- });
-
- it('hides the container', () => {
- expect(innerContainer.style.display).toEqual('none');
- });
-
- it('call component.render()', () => {
- expect(component.render).toHaveBeenCalled();
- });
-
- describe('when set back to false', () => {
- beforeEach(() => {
- props.shouldAutoHideContainer = false;
- ReactDOM.render( , container);
- });
-
- it('shows the container', () => {
- expect(innerContainer.style.display).not.toEqual('none');
- });
-
- it('calls component.render()', () => {
- expect(component.render).toHaveBeenCalledTimes(2);
- });
- });
- });
- });
-});
diff --git a/src/decorators/__tests__/headerFooter-test.js b/src/decorators/__tests__/headerFooter-test.js
deleted file mode 100644
index eb702bd560..0000000000
--- a/src/decorators/__tests__/headerFooter-test.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { Component } from 'react';
-import expect from 'expect';
-import { shallow } from 'enzyme';
-import { createRenderer } from 'react-test-renderer/shallow';
-
-import headerFooter from '../headerFooter';
-
-class TestComponent extends Component {
- render() {
- return
;
- }
-}
-
-describe('headerFooter', () => {
- let renderer;
- let defaultProps;
-
- function render(props = {}) {
- const HeaderFooter = headerFooter(TestComponent);
- renderer.render( );
- return renderer.getRenderOutput();
- }
-
- function shallowRender(extraProps = {}) {
- const props = {
- templateProps: {},
- ...extraProps,
- };
- const componentWrappedInHeaderFooter = headerFooter(TestComponent);
- return shallow(React.createElement(componentWrappedInHeaderFooter, props));
- }
-
- beforeEach(() => {
- defaultProps = {
- cssClasses: {
- root: 'root',
- body: 'body',
- },
- collapsible: false,
- templateProps: {},
- };
- renderer = createRenderer();
- });
-
- it('should render the component in a root and body', () => {
- const out = render(defaultProps);
- expect(out).toMatchSnapshot();
- });
-
- it('should add a header if such a template is passed', () => {
- // Given
- defaultProps.templateProps.templates = {
- header: 'HEADER',
- };
- // When
- const out = render(defaultProps);
- // Then
- expect(out).toMatchSnapshot();
- });
-
- it('should add a footer if such a template is passed', () => {
- // Given
- defaultProps.templateProps.templates = {
- footer: 'FOOTER',
- };
- // When
- const out = render(defaultProps);
- // Then
- expect(out).toMatchSnapshot();
- });
-
- describe('collapsible', () => {
- beforeEach(() => {
- defaultProps.templateProps.templates = {
- header: 'yo header',
- footer: 'yo footer',
- };
- });
-
- it('when true', () => {
- defaultProps.collapsible = true;
- const out = render(defaultProps);
- expect(out).toMatchSnapshot();
- });
-
- it('when collapsed', () => {
- defaultProps.collapsible = { collapsed: true };
- const out = render(defaultProps);
- expect(out).toMatchSnapshot();
- });
- });
-
- describe('headerFooterData', () => {
- it('should call the header and footer template with the given data', () => {
- // Given
- const props = {
- headerFooterData: {
- header: {
- foo: 'bar',
- },
- footer: {
- foo: 'baz',
- },
- },
- templateProps: {
- templates: {
- header: 'header',
- footer: 'footer',
- },
- },
- };
-
- // When
- const actual = shallowRender(props);
- const header = actual.find({ templateKey: 'header' });
- const footer = actual.find({ templateKey: 'footer' });
-
- // Then
- expect(header.props().data.foo).toEqual('bar');
- expect(footer.props().data.foo).toEqual('baz');
- });
- });
-});
diff --git a/src/decorators/autoHideContainer.js b/src/decorators/autoHideContainer.js
deleted file mode 100644
index 6590f4b0d7..0000000000
--- a/src/decorators/autoHideContainer.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'preact-compat';
-
-export default function(ComposedComponent) {
- return class AutoHide extends Component {
- static displayName = `${ComposedComponent.name}-AutoHide`;
- static propTypes = { shouldAutoHideContainer: PropTypes.bool.isRequired };
-
- render() {
- const { shouldAutoHideContainer } = this.props;
- return (
-
-
-
- );
- }
- };
-}
diff --git a/src/decorators/headerFooter.js b/src/decorators/headerFooter.js
deleted file mode 100644
index fc6ad94a47..0000000000
--- a/src/decorators/headerFooter.js
+++ /dev/null
@@ -1,113 +0,0 @@
-import PropTypes from 'prop-types';
-// Issue with eslint + high-order components like decorators
-/* eslint react/prop-types: 0 */
-
-import React, { Component } from 'preact-compat';
-
-import cx from 'classnames';
-import getKey from 'lodash/get';
-
-import Template from '../components/Template.js';
-
-function headerFooter(ComposedComponent) {
- class HeaderFooter extends Component {
- constructor(props) {
- super(props);
- this.handleHeaderClick = this.handleHeaderClick.bind(this);
- this.state = {
- collapsed: props.collapsible && props.collapsible.collapsed,
- };
-
- this._cssClasses = {
- root: cx('ais-root', this.props.cssClasses.root),
- body: cx('ais-body', this.props.cssClasses.body),
- };
-
- this._footerElement = this._getElement({ type: 'footer' });
- }
- _getElement({ type, handleClick = null }) {
- const templates =
- this.props.templateProps && this.props.templateProps.templates;
- if (!templates || !templates[type]) {
- return null;
- }
- const className = cx(this.props.cssClasses[type], `ais-${type}`);
-
- const templateData = getKey(this.props, `headerFooterData.${type}`);
-
- return (
-
- );
- }
- handleHeaderClick() {
- this.setState({
- collapsed: !this.state.collapsed,
- });
- }
- render() {
- const rootCssClasses = [this._cssClasses.root];
-
- if (this.props.collapsible) {
- rootCssClasses.push('ais-root__collapsible');
- }
-
- if (this.state.collapsed) {
- rootCssClasses.push('ais-root__collapsed');
- }
-
- const cssClasses = {
- ...this._cssClasses,
- root: cx(rootCssClasses),
- };
-
- const headerElement = this._getElement({
- type: 'header',
- handleClick: this.props.collapsible ? this.handleHeaderClick : null,
- });
-
- return (
-
- {headerElement}
-
-
-
- {this._footerElement}
-
- );
- }
- }
-
- HeaderFooter.propTypes = {
- collapsible: PropTypes.oneOfType([
- PropTypes.bool,
- PropTypes.shape({
- collapsed: PropTypes.bool,
- }),
- ]),
- cssClasses: PropTypes.shape({
- root: PropTypes.string,
- header: PropTypes.string,
- body: PropTypes.string,
- footer: PropTypes.string,
- }),
- templateProps: PropTypes.object,
- };
-
- HeaderFooter.defaultProps = {
- cssClasses: {},
- collapsible: false,
- };
-
- // precise displayName for ease of debugging (react dev tool, react warnings)
- HeaderFooter.displayName = `${ComposedComponent.name}-HeaderFooter`;
-
- return HeaderFooter;
-}
-
-export default headerFooter;
diff --git a/src/helpers/__tests__/highlight-test.js b/src/helpers/__tests__/highlight-test.js
new file mode 100644
index 0000000000..78bb1de89a
--- /dev/null
+++ b/src/helpers/__tests__/highlight-test.js
@@ -0,0 +1,113 @@
+import highlight from '../highlight';
+
+/* eslint-disable camelcase */
+const hit = {
+ name: 'Amazon - Fire TV Stick with Alexa Voice Remote - Black',
+ description:
+ 'Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming.',
+ brand: 'Amazon',
+ categories: ['TV & Home Theater', 'Streaming Media Players'],
+ hierarchicalCategories: {
+ lvl0: 'TV & Home Theater',
+ lvl1: 'TV & Home Theater > Streaming Media Players',
+ },
+ type: 'Streaming media plyr',
+ price: 39.99,
+ price_range: '1 - 50',
+ image: 'https://cdn-demo.algolia.com/bestbuy-0118/5477500_sb.jpg',
+ url: 'https://api.bestbuy.com/click/-/5477500/pdp',
+ free_shipping: false,
+ rating: 4,
+ popularity: 21469,
+ objectID: '5477500',
+ _highlightResult: {
+ name: {
+ value:
+ 'Amazon - Fire TV Stick with Alexa Voice Remote - Black',
+ matchLevel: 'full',
+ fullyHighlighted: false,
+ matchedWords: ['amazon'],
+ },
+ description: {
+ value:
+ 'Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming.',
+ matchLevel: 'full',
+ fullyHighlighted: false,
+ matchedWords: ['amazon'],
+ },
+ brand: {
+ value: 'Amazon ',
+ matchLevel: 'full',
+ fullyHighlighted: true,
+ matchedWords: ['amazon'],
+ },
+ categories: [
+ {
+ value: 'TV & Home Theater',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ {
+ value: 'Streaming Media Players',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ ],
+ type: {
+ value: 'Streaming media plyr',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ meta: {
+ name: {
+ value: 'Nested Amazon name',
+ },
+ },
+ },
+};
+/* eslint-enable camelcase */
+
+describe('highlight', () => {
+ test('with default tag name', () => {
+ expect(
+ highlight({
+ attribute: 'name',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Amazon - Fire TV Stick with Alexa Voice Remote - Black"`
+ );
+ });
+
+ test('with custom tag name', () => {
+ expect(
+ highlight({
+ attribute: 'description',
+ highlightedTagName: 'em',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming."`
+ );
+ });
+
+ test('with unknown attribute returns an empty string', () => {
+ expect(
+ highlight({
+ attribute: 'wrong-attribute',
+ hit,
+ })
+ ).toMatchInlineSnapshot(`""`);
+ });
+
+ test('with nested attribute', () => {
+ expect(
+ highlight({
+ attribute: 'meta.name',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Nested Amazon name"`
+ );
+ });
+});
diff --git a/src/helpers/__tests__/snippet-test.js b/src/helpers/__tests__/snippet-test.js
new file mode 100644
index 0000000000..e05a9d1374
--- /dev/null
+++ b/src/helpers/__tests__/snippet-test.js
@@ -0,0 +1,113 @@
+import snippet from '../snippet';
+
+/* eslint-disable camelcase */
+const hit = {
+ name: 'Amazon - Fire TV Stick with Alexa Voice Remote - Black',
+ description:
+ 'Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming.',
+ brand: 'Amazon',
+ categories: ['TV & Home Theater', 'Streaming Media Players'],
+ hierarchicalCategories: {
+ lvl0: 'TV & Home Theater',
+ lvl1: 'TV & Home Theater > Streaming Media Players',
+ },
+ type: 'Streaming media plyr',
+ price: 39.99,
+ price_range: '1 - 50',
+ image: 'https://cdn-demo.algolia.com/bestbuy-0118/5477500_sb.jpg',
+ url: 'https://api.bestbuy.com/click/-/5477500/pdp',
+ free_shipping: false,
+ rating: 4,
+ popularity: 21469,
+ objectID: '5477500',
+ _snippetResult: {
+ name: {
+ value:
+ 'Amazon - Fire TV Stick with Alexa Voice Remote - Black',
+ matchLevel: 'full',
+ fullyHighlighted: false,
+ matchedWords: ['amazon'],
+ },
+ description: {
+ value:
+ 'Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming.',
+ matchLevel: 'full',
+ fullyHighlighted: false,
+ matchedWords: ['amazon'],
+ },
+ brand: {
+ value: 'Amazon ',
+ matchLevel: 'full',
+ fullyHighlighted: true,
+ matchedWords: ['amazon'],
+ },
+ categories: [
+ {
+ value: 'TV & Home Theater',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ {
+ value: 'Streaming Media Players',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ ],
+ type: {
+ value: 'Streaming media plyr',
+ matchLevel: 'none',
+ matchedWords: [],
+ },
+ meta: {
+ name: {
+ value: 'Nested Amazon name',
+ },
+ },
+ },
+};
+/* eslint-enable camelcase */
+
+describe('snippet', () => {
+ test('with default tag name', () => {
+ expect(
+ snippet({
+ attribute: 'name',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Amazon - Fire TV Stick with Alexa Voice Remote - Black"`
+ );
+ });
+
+ test('with custom tag name', () => {
+ expect(
+ snippet({
+ attribute: 'description',
+ highlightedTagName: 'em',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Enjoy smart access to videos, games and apps with this Amazon Fire TV stick. Its Alexa voice remote lets you deliver hands-free commands when you want to watch television or engage with other applications. With a quad-core processor, 1GB internal memory and 8GB of storage, this portable Amazon Fire TV stick works fast for buffer-free streaming."`
+ );
+ });
+
+ test('with unknown attribute returns an empty string', () => {
+ expect(
+ snippet({
+ attribute: 'wrong-attribute',
+ hit,
+ })
+ ).toMatchInlineSnapshot(`""`);
+ });
+
+ test('with nested attribute', () => {
+ expect(
+ snippet({
+ attribute: 'meta.name',
+ hit,
+ })
+ ).toMatchInlineSnapshot(
+ `"Nested Amazon name"`
+ );
+ });
+});
diff --git a/src/helpers/highlight.js b/src/helpers/highlight.js
new file mode 100644
index 0000000000..8628b40778
--- /dev/null
+++ b/src/helpers/highlight.js
@@ -0,0 +1,28 @@
+import { getPropertyByPath } from '../lib/utils';
+import { TAG_REPLACEMENT } from '../lib/escape-highlight';
+import { component } from '../lib/suit';
+
+const suit = component('Highlight');
+
+export default function highlight({
+ attribute,
+ highlightedTagName = 'mark',
+ hit,
+} = {}) {
+ const attributeValue =
+ getPropertyByPath(hit, `_highlightResult.${attribute}.value`) || '';
+
+ const className = suit({
+ descendantName: 'highlighted',
+ });
+
+ return attributeValue
+ .replace(
+ new RegExp(TAG_REPLACEMENT.highlightPreTag, 'g'),
+ `<${highlightedTagName} class="${className}">`
+ )
+ .replace(
+ new RegExp(TAG_REPLACEMENT.highlightPostTag, 'g'),
+ `${highlightedTagName}>`
+ );
+}
diff --git a/src/helpers/index.js b/src/helpers/index.js
new file mode 100644
index 0000000000..49d493cbd4
--- /dev/null
+++ b/src/helpers/index.js
@@ -0,0 +1,2 @@
+export { default as highlight } from './highlight.js';
+export { default as snippet } from './snippet.js';
diff --git a/src/helpers/snippet.js b/src/helpers/snippet.js
new file mode 100644
index 0000000000..c20f9fa402
--- /dev/null
+++ b/src/helpers/snippet.js
@@ -0,0 +1,28 @@
+import { getPropertyByPath } from '../lib/utils';
+import { TAG_REPLACEMENT } from '../lib/escape-highlight';
+import { component } from '../lib/suit';
+
+const suit = component('Snippet');
+
+export default function snippet({
+ attribute,
+ highlightedTagName = 'mark',
+ hit,
+} = {}) {
+ const attributeValue =
+ getPropertyByPath(hit, `_snippetResult.${attribute}.value`) || '';
+
+ const className = suit({
+ descendantName: 'highlighted',
+ });
+
+ return attributeValue
+ .replace(
+ new RegExp(TAG_REPLACEMENT.highlightPreTag, 'g'),
+ `<${highlightedTagName} class="${className}">`
+ )
+ .replace(
+ new RegExp(TAG_REPLACEMENT.highlightPostTag, 'g'),
+ `${highlightedTagName}>`
+ );
+}
diff --git a/src/index.es.js b/src/index.es.js
new file mode 100644
index 0000000000..fb166c29d3
--- /dev/null
+++ b/src/index.es.js
@@ -0,0 +1,36 @@
+import toFactory from 'to-factory';
+import InstantSearch from './lib/InstantSearch';
+import version from './lib/version.js';
+import { snippet, highlight } from './helpers';
+
+const instantSearchFactory = toFactory(InstantSearch);
+
+instantSearchFactory.version = version;
+instantSearchFactory.snippet = snippet;
+instantSearchFactory.highlight = highlight;
+
+Object.defineProperty(instantSearchFactory, 'widgets', {
+ get() {
+ throw new ReferenceError(
+ `"instantsearch.widgets" are not available from the ES build.
+
+To import the widgets:
+
+import { searchBox } from 'instantsearch.js/es/widgets'`
+ );
+ },
+});
+
+Object.defineProperty(instantSearchFactory, 'connectors', {
+ get() {
+ throw new ReferenceError(
+ `"instantsearch.connectors" are not available from the ES build.
+
+To import the connectors:
+
+import { connectSearchBox } from 'instantsearch.js/es/connectors'`
+ );
+ },
+});
+
+export default instantSearchFactory;
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000000..c3d8b56fb4
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,3 @@
+import instantsearch from './lib/main.js';
+
+export default instantsearch;
diff --git a/src/lib/InstantSearch.js b/src/lib/InstantSearch.js
index 10161f94a0..344acb5b1b 100644
--- a/src/lib/InstantSearch.js
+++ b/src/lib/InstantSearch.js
@@ -1,19 +1,15 @@
// we use the full path to the lite build to solve a meteor.js issue:
// https://github.com/algolia/instantsearch.js/issues/1024#issuecomment-221618284
-import algoliasearch from 'algoliasearch/src/browser/builds/algoliasearchLite.js';
import algoliasearchHelper from 'algoliasearch-helper';
-import forEach from 'lodash/forEach';
import mergeWith from 'lodash/mergeWith';
import union from 'lodash/union';
import isPlainObject from 'lodash/isPlainObject';
import EventEmitter from 'events';
-import urlSyncWidget from './url-sync.js';
import RoutingManager from './RoutingManager.js';
import simpleMapping from './stateMappings/simple.js';
import historyRouter from './routers/history.js';
import version from './version.js';
import createHelpers from './createHelpers.js';
-import { warn } from './utils';
const ROUTING_DEFAULT_OPTIONS = {
stateMapping: simpleMapping(),
@@ -23,41 +19,6 @@ const ROUTING_DEFAULT_OPTIONS = {
function defaultCreateURL() {
return '#';
}
-const defaultCreateAlgoliaClient = (defaultAlgoliasearch, appId, apiKey) =>
- defaultAlgoliasearch(appId, apiKey);
-
-const checkOptions = ({
- appId,
- apiKey,
- indexName,
- createAlgoliaClient,
- searchClient,
-}) => {
- if (!searchClient) {
- if (appId === null || apiKey === null || indexName === null) {
- const usage = `
-Usage: instantsearch({
- appId: 'my_application_id',
- apiKey: 'my_search_api_key',
- indexName: 'my_index_name'
-});`;
- throw new Error(usage);
- }
- } else if (
- searchClient &&
- (indexName === null ||
- appId !== null ||
- apiKey !== null ||
- createAlgoliaClient !== defaultCreateAlgoliaClient)
- ) {
- const usage = `
-Usage: instantsearch({
- indexName: 'my_index_name',
- searchClient: algoliasearch('appId', 'apiKey')
-});`;
- throw new Error(usage);
- }
-};
/**
* Widgets are the building blocks of InstantSearch.js. Any
@@ -79,41 +40,39 @@ class InstantSearch extends EventEmitter {
super();
const {
- appId = null,
- apiKey = null,
indexName = null,
numberLocale,
searchParameters = {},
- urlSync = null,
routing = null,
searchFunction,
- createAlgoliaClient = defaultCreateAlgoliaClient,
stalledSearchDelay = 200,
searchClient = null,
} = options;
- checkOptions({
- appId,
- apiKey,
- indexName,
- createAlgoliaClient,
- searchClient,
- });
+ if (indexName === null || searchClient === null) {
+ throw new Error(`Usage: instantsearch({
+ indexName: 'indexName',
+ searchClient: algoliasearch('appId', 'apiKey')
+});`);
+ }
- if (searchClient && typeof searchClient.search !== 'function') {
+ if (typeof options.urlSync !== 'undefined') {
throw new Error(
- 'InstantSearch configuration error: `searchClient` must implement a `search(requests)` method.'
+ 'InstantSearch.js V3: `urlSync` option has been removed. You can now use the new `routing` option'
);
}
- const client =
- searchClient || createAlgoliaClient(algoliasearch, appId, apiKey);
+ if (typeof searchClient.search !== 'function') {
+ throw new Error(
+ 'The search client must implement a `search(requests)` method.'
+ );
+ }
- if (typeof client.addAlgoliaAgent === 'function') {
- client.addAlgoliaAgent(`instantsearch.js ${version}`);
+ if (typeof searchClient.addAlgoliaAgent === 'function') {
+ searchClient.addAlgoliaAgent(`instantsearch.js ${version}`);
}
- this.client = client;
+ this.client = searchClient;
this.helper = null;
this.indexName = indexName;
this.searchParameters = { ...searchParameters, index: indexName };
@@ -128,42 +87,12 @@ class InstantSearch extends EventEmitter {
this._searchFunction = searchFunction;
}
- if (urlSync !== null) {
- if (routing !== null) {
- throw new Error(
- 'InstantSearch configuration error: it is not possible to use `urlSync` and `routing` at the same time'
- );
- }
-
- warn(
- '`urlSync` option is deprecated and will be removed in the next major version.\n' +
- 'You can now use the new `routing` option.'
- );
-
- if (urlSync === true) {
- // when using urlSync: true
- warn('Use it like this: `routing: true`');
- }
-
- warn(
- 'For advanced use cases, checkout the documentation: https://community.algolia.com/instantsearch.js/v2/guides/routing.html#migrating-from-urlsync'
- );
- }
-
- this.urlSync = urlSync === true ? {} : urlSync;
if (routing === true) this.routing = ROUTING_DEFAULT_OPTIONS;
else if (isPlainObject(routing))
this.routing = {
...ROUTING_DEFAULT_OPTIONS,
...routing,
};
-
- if (options.createAlgoliaClient) {
- warn(`
-\`createAlgoliaClient\` option is deprecated and will be removed in the next major version.
-Please use \`searchClient\` instead: https://community.algolia.com/instantsearch.js/v2/instantsearch.html#struct-InstantSearchOptions-searchClient.
-To help you migrate, please refer to the migration guide: https://community.algolia.com/instantsearch.js/v2/guides/prepare-for-v3.html`);
- }
}
/**
@@ -282,11 +211,9 @@ To help you migrate, please refer to the migration guide: https://community.algo
// re-compute remaining widgets to the state
// in a case two widgets were using the same configuration but we removed one
if (nextState) {
- // We don't want to re-add URlSync `getConfiguration` widget
- // it can throw errors since it may re-add SearchParameters about something unmounted
- this.searchParameters = this.widgets
- .filter(w => w.constructor.name !== 'URLSync')
- .reduce(enhanceConfiguration({}), { ...nextState });
+ this.searchParameters = this.widgets.reduce(enhanceConfiguration({}), {
+ ...nextState,
+ });
this.helper.setState(this.searchParameters);
}
@@ -324,22 +251,11 @@ To help you migrate, please refer to the migration guide: https://community.algo
* @return {undefined} Does not return anything
*/
start() {
- if (!this.widgets)
- throw new Error('No widgets were added to instantsearch.js');
-
if (this.started) throw new Error('start() has been already called once');
let searchParametersFromUrl;
- if (this.urlSync) {
- const syncWidget = urlSyncWidget(this.urlSync);
- this._createURL = syncWidget.createURL.bind(syncWidget);
- this._createAbsoluteURL = relative =>
- this._createURL(relative, { absolute: true });
- this._onHistoryChange = syncWidget.onHistoryChange.bind(syncWidget);
- this.widgets.push(syncWidget);
- searchParametersFromUrl = syncWidget.searchParametersFromUrl;
- } else if (this.routing) {
+ if (this.routing) {
const routingManager = new RoutingManager({
...this.routing,
instantSearchInstance: this,
@@ -439,7 +355,7 @@ To help you migrate, please refer to the migration guide: https://community.algo
this._isSearchStalled = false;
}
- forEach(this.widgets, widget => {
+ this.widgets.forEach(widget => {
if (!widget.render) {
return;
}
@@ -465,7 +381,7 @@ To help you migrate, please refer to the migration guide: https://community.algo
}
_init(state, helper) {
- forEach(this.widgets, widget => {
+ this.widgets.forEach(widget => {
if (widget.init) {
widget.init({
state,
diff --git a/src/lib/__tests__/InstantSearch-test-2.js b/src/lib/__tests__/InstantSearch-test-2.js
index 62b4f27972..a231e7cf23 100644
--- a/src/lib/__tests__/InstantSearch-test-2.js
+++ b/src/lib/__tests__/InstantSearch-test-2.js
@@ -3,8 +3,6 @@ import InstantSearch from '../InstantSearch';
jest.useFakeTimers();
-const appId = 'appId';
-const apiKey = 'apiKey';
const indexName = 'lifecycle';
describe('InstantSearch life cycle', () => {
@@ -18,11 +16,9 @@ describe('InstantSearch life cycle', () => {
};
const search = new InstantSearch({
- appId,
- apiKey,
indexName,
searchFunction: searchFunctionSpy,
- createAlgoliaClient: () => fakeClient,
+ searchClient: fakeClient,
});
expect(searchFunctionSpy).not.toHaveBeenCalled();
@@ -63,10 +59,8 @@ describe('InstantSearch life cycle', () => {
};
const search = new InstantSearch({
- appId,
- apiKey,
indexName,
- createAlgoliaClient: () => fakeClient,
+ searchClient: fakeClient,
});
const widget = {
@@ -144,11 +138,9 @@ describe('InstantSearch life cycle', () => {
};
const search = new InstantSearch({
- appId,
- apiKey,
indexName,
searchFunction: searchFunctionSpy,
- createAlgoliaClient: () => fakeClient,
+ searchClient: fakeClient,
});
search.start();
@@ -161,10 +153,13 @@ describe('InstantSearch life cycle', () => {
});
it('does not break when providing searchFunction with multiple resquests', () => {
+ const fakeClient = {
+ search: jest.fn(() => Promise.resolve({ results: [{}, {}] })),
+ };
+
const search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: fakeClient,
searchFunction: h => {
h.addDisjunctiveFacetRefinement('brand', 'Apple');
h.search();
diff --git a/src/lib/__tests__/InstantSearch-test-integration.js b/src/lib/__tests__/InstantSearch-test-integration.js
index 9bb846c728..1eaaf51d67 100644
--- a/src/lib/__tests__/InstantSearch-test-integration.js
+++ b/src/lib/__tests__/InstantSearch-test-integration.js
@@ -1,14 +1,17 @@
// import algoliaSearchHelper from 'algoliasearch-helper';
+import algoliasearch from 'algoliasearch';
import InstantSearch from '../InstantSearch';
describe('InstantSearch lifecycle', () => {
it('emits an error if the API returns an error', () => {
const search = new InstantSearch({
- // correct credentials so that the client does not retry
- appId: 'latency',
- apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
// the index name does not exist so that we get an error
indexName: 'DOESNOTEXIST',
+ // correct credentials so that the client does not retry
+ searchClient: algoliasearch(
+ 'latency',
+ '6be0576ff61c053d5f9a3225e2a90f76'
+ ),
});
let sendError;
diff --git a/src/lib/__tests__/InstantSearch-test.js b/src/lib/__tests__/InstantSearch-test.js
index 4ac204a7ba..311ffcb6df 100644
--- a/src/lib/__tests__/InstantSearch-test.js
+++ b/src/lib/__tests__/InstantSearch-test.js
@@ -1,7 +1,5 @@
import range from 'lodash/range';
import times from 'lodash/times';
-import sinon from 'sinon';
-
import algoliaSearchHelper from 'algoliasearch-helper';
import InstantSearch from '../InstantSearch';
@@ -16,28 +14,20 @@ describe('InstantSearch lifecycle', () => {
let searchParameters;
let search;
let helperSearchSpy;
- let urlSync;
beforeEach(() => {
- client = { algolia: 'client' };
+ client = { search() {} };
helper = algoliaSearchHelper(client);
// when using searchFunction, we lose the reference to
// the original helper.search
- const spy = sinon.spy();
+ const spy = jest.fn();
helper.search = spy;
helperSearchSpy = spy;
- urlSync = {
- createURL: sinon.spy(),
- onHistoryChange: () => {},
- getConfiguration: sinon.spy(),
- render: () => {},
- };
-
- algoliasearch = sinon.stub().returns(client);
- helperStub = sinon.stub().returns(helper);
+ algoliasearch = jest.fn().mockReturnValue(client);
+ helperStub = jest.fn().mockReturnValue(helper);
appId = 'appId';
apiKey = 'apiKey';
@@ -50,77 +40,28 @@ describe('InstantSearch lifecycle', () => {
another: { config: 'parameter' },
};
- InstantSearch.__Rewire__('urlSyncWidget', () => urlSync);
InstantSearch.__Rewire__('algoliasearch', algoliasearch);
InstantSearch.__Rewire__('algoliasearchHelper', helperStub);
search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: algoliasearch(appId, apiKey),
searchParameters,
- urlSync: {},
});
});
afterEach(() => {
- InstantSearch.__ResetDependency__('urlSyncWidget');
InstantSearch.__ResetDependency__('algoliasearch');
InstantSearch.__ResetDependency__('algoliasearchHelper');
});
it('calls algoliasearch(appId, apiKey)', () => {
- expect(algoliasearch.calledOnce).toBe(true, 'algoliasearch called once');
- expect(algoliasearch.args[0]).toEqual([appId, apiKey]);
+ expect(algoliasearch).toHaveBeenCalledTimes(1);
+ expect(algoliasearch).toHaveBeenCalledWith(appId, apiKey);
});
it('does not call algoliasearchHelper', () => {
- expect(helperStub.notCalled).toBe(
- true,
- 'algoliasearchHelper not yet called'
- );
- });
-
- describe('when providing a custom client module', () => {
- let createAlgoliaClient;
- let customAppID;
- let customApiKey;
-
- beforeEach(() => {
- // InstantSearch is being called once at the top-level context, so reset the `algoliasearch` spy
- algoliasearch.resetHistory();
-
- // Create a spy to act as a clientInstanceFunction that returns a custom client
- createAlgoliaClient = sinon.stub().returns(client);
- customAppID = 'customAppID';
- customApiKey = 'customAPIKey';
-
- // Create a new InstantSearch instance with custom client function
- search = new InstantSearch({
- appId: customAppID,
- apiKey: customApiKey,
- indexName,
- searchParameters,
- urlSync: {},
- createAlgoliaClient,
- });
- });
-
- it('does not call algoliasearch directly', () => {
- expect(algoliasearch.calledOnce).toBe(false, 'algoliasearch not called');
- });
-
- it('calls createAlgoliaClient(appId, apiKey)', () => {
- expect(createAlgoliaClient.calledOnce).toBe(
- true,
- 'clientInstanceFunction called once'
- );
- expect(createAlgoliaClient.args[0]).toEqual([
- algoliasearch,
- customAppID,
- customApiKey,
- ]);
- });
+ expect(helperStub).not.toHaveBeenCalled();
});
describe('when adding a widget without render and init', () => {
@@ -141,9 +82,8 @@ describe('InstantSearch lifecycle', () => {
const disjunctiveFacetsRefinements = { fruits: ['apple'] };
const facetsRefinements = disjunctiveFacetsRefinements;
search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: algoliasearch(appId, apiKey),
searchParameters: {
disjunctiveFacetsRefinements,
facetsRefinements,
@@ -166,19 +106,20 @@ describe('InstantSearch lifecycle', () => {
beforeEach(() => {
widget = {
- getConfiguration: sinon
- .stub()
- .returns({ some: 'modified', another: { different: 'parameter' } }),
- init: sinon.spy(() => {
+ getConfiguration: jest.fn().mockReturnValue({
+ some: 'modified',
+ another: { different: 'parameter' },
+ }),
+ init: jest.fn(() => {
helper.state.sendMeToUrlSync = true;
}),
- render: sinon.spy(),
+ render: jest.fn(),
};
search.addWidget(widget);
});
it('does not call widget.getConfiguration', () => {
- expect(widget.getConfiguration.notCalled).toBe(true);
+ expect(widget.getConfiguration).not.toHaveBeenCalled();
});
describe('when we call search.start', () => {
@@ -187,58 +128,38 @@ describe('InstantSearch lifecycle', () => {
});
it('calls widget.getConfiguration(searchParameters)', () => {
- expect(widget.getConfiguration.args[0]).toEqual([
+ expect(widget.getConfiguration).toHaveBeenCalledWith(
searchParameters,
- undefined,
- ]);
+ undefined
+ );
});
it('calls algoliasearchHelper(client, indexName, searchParameters)', () => {
- expect(helperStub.calledOnce).toBe(
- true,
- 'algoliasearchHelper called once'
- );
- expect(helperStub.args[0]).toEqual([
- client,
- indexName,
- {
- some: 'modified',
- values: [-2, -1],
- index: indexName,
- another: { different: 'parameter', config: 'parameter' },
- },
- ]);
+ expect(helperStub).toHaveBeenCalledTimes(1);
+ expect(helperStub).toHaveBeenCalledWith(client, indexName, {
+ some: 'modified',
+ values: [-2, -1],
+ index: indexName,
+ another: { different: 'parameter', config: 'parameter' },
+ });
});
it('calls helper.search()', () => {
- expect(helperSearchSpy.calledOnce).toBe(true);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
});
it('calls widget.init(helper.state, helper, templatesConfig)', () => {
- expect(widget.init.calledOnce).toBe(true, 'widget.init called once');
- expect(widget.init.calledAfter(widget.getConfiguration)).toBe(
- true,
- 'widget.init() was called after widget.getConfiguration()'
- );
- const args = widget.init.args[0][0];
+ expect(widget.getConfiguration).toHaveBeenCalledTimes(1);
+ expect(widget.init).toHaveBeenCalledTimes(1);
+ const args = widget.init.mock.calls[0][0];
expect(args.state).toBe(helper.state);
expect(args.helper).toBe(helper);
expect(args.templatesConfig).toBe(search.templatesConfig);
expect(args.onHistoryChange).toBe(search._onHistoryChange);
});
- it('calls urlSync.getConfiguration after every widget', () => {
- expect(urlSync.getConfiguration.calledOnce).toBe(
- true,
- 'urlSync.getConfiguration called once'
- );
- expect(
- urlSync.getConfiguration.calledAfter(widget.getConfiguration)
- ).toBe(true, 'urlSync.getConfiguration was called after widget.init');
- });
-
it('does not call widget.render', () => {
- expect(widget.render.notCalled).toBe(true);
+ expect(widget.render).not.toHaveBeenCalled();
});
describe('when we have results', () => {
@@ -250,11 +171,8 @@ describe('InstantSearch lifecycle', () => {
});
it('calls widget.render({results, state, helper, templatesConfig, instantSearchInstance})', () => {
- expect(widget.render.calledOnce).toBe(
- true,
- 'widget.render called once'
- );
- expect(widget.render.args[0]).toMatchSnapshot();
+ expect(widget.render).toHaveBeenCalledTimes(1);
+ expect(widget.render.mock.calls[0]).toMatchSnapshot();
});
});
});
@@ -266,41 +184,29 @@ describe('InstantSearch lifecycle', () => {
beforeEach(() => {
widgets = range(5).map((widget, widgetIndex) => ({
init() {},
- getConfiguration: sinon.stub().returns({ values: [widgetIndex] }),
+ getConfiguration: jest.fn().mockReturnValue({ values: [widgetIndex] }),
}));
search.addWidgets(widgets);
search.start();
});
- it('calls widget[x].getConfiguration in the orders the widgets were added', () => {
- const order = widgets.every((widget, widgetIndex, filteredWidgets) => {
- if (widgetIndex === 0) {
- return (
- widget.getConfiguration.calledOnce &&
- widget.getConfiguration.calledBefore(
- filteredWidgets[1].getConfiguration
- )
- );
- }
- const previousWidget = filteredWidgets[widgetIndex - 1];
- return (
- widget.getConfiguration.calledOnce &&
- widget.getConfiguration.calledAfter(previousWidget.getConfiguration)
- );
- });
-
- expect(order).toBe(true);
- });
-
it('recursively merges searchParameters.values array', () => {
- expect(helperStub.args[0][2].values).toEqual([-2, -1, 0, 1, 2, 3, 4]);
+ expect(helperStub.mock.calls[0][2].values).toEqual([
+ -2,
+ -1,
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ ]);
});
});
describe('when render happens', () => {
- const render = sinon.spy();
+ const render = jest.fn();
beforeEach(() => {
- render.resetHistory();
+ render.mockReset();
const widgets = range(5).map(() => ({ render }));
widgets.forEach(search.addWidget, search);
@@ -308,48 +214,40 @@ describe('InstantSearch lifecycle', () => {
search.start();
});
- it('has a createURL method', () => {
- search.createURL({ hitsPerPage: 542 });
- expect(urlSync.createURL.calledOnce).toBe(true);
- expect(urlSync.createURL.getCall(0).args[0].hitsPerPage).toBe(542);
- });
-
it('emits render when all render are done (using on)', () => {
- const onRender = sinon.spy();
+ const onRender = jest.fn();
search.on('render', onRender);
- expect(render.callCount).toEqual(0);
- expect(onRender.callCount).toEqual(0);
+ expect(render).toHaveBeenCalledTimes(0);
+ expect(onRender).toHaveBeenCalledTimes(0);
helper.emit('result', {}, helper.state);
- expect(render.callCount).toEqual(5);
- expect(onRender.callCount).toEqual(1);
- expect(render.calledBefore(onRender)).toBe(true);
+ expect(render).toHaveBeenCalledTimes(5);
+ expect(onRender).toHaveBeenCalledTimes(1);
helper.emit('result', {}, helper.state);
- expect(render.callCount).toEqual(10);
- expect(onRender.callCount).toEqual(2);
+ expect(render).toHaveBeenCalledTimes(10);
+ expect(onRender).toHaveBeenCalledTimes(2);
});
it('emits render when all render are done (using once)', () => {
- const onRender = sinon.spy();
+ const onRender = jest.fn();
search.once('render', onRender);
- expect(render.callCount).toEqual(0);
- expect(onRender.callCount).toEqual(0);
+ expect(render).toHaveBeenCalledTimes(0);
+ expect(onRender).toHaveBeenCalledTimes(0);
helper.emit('result', {}, helper.state);
- expect(render.callCount).toEqual(5);
- expect(onRender.callCount).toEqual(1);
- expect(render.calledBefore(onRender)).toBe(true);
+ expect(render).toHaveBeenCalledTimes(5);
+ expect(onRender).toHaveBeenCalledTimes(1);
helper.emit('result', {}, helper.state);
- expect(render.callCount).toEqual(10);
- expect(onRender.callCount).toEqual(1);
+ expect(render).toHaveBeenCalledTimes(10);
+ expect(onRender).toHaveBeenCalledTimes(1);
});
});
@@ -375,9 +273,8 @@ describe('InstantSearch lifecycle', () => {
beforeEach(() => {
search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: algoliasearch(appId, apiKey),
});
});
@@ -516,55 +413,14 @@ describe('InstantSearch lifecycle', () => {
expect(search.searchParameters.numericRefinements).toEqual({});
expect(search.searchParameters.disjunctiveFacets).toEqual([]);
});
-
- it('should unmount a widget without calling URLSync widget getConfiguration', () => {
- // fake url-sync widget
- const spy = jest.fn();
-
- class URLSync {
- constructor() {
- this.getConfiguration = spy;
- this.init = jest.fn();
- this.render = jest.fn();
- this.dispose = jest.fn();
- }
- }
-
- const urlSyncWidget = new URLSync();
- expect(urlSyncWidget.constructor.name).toEqual('URLSync');
-
- search.addWidget(urlSyncWidget);
-
- // add fake widget to dispose
- // that returns a `nextState` while dispose
- const widget1 = registerWidget(
- undefined,
- jest.fn(({ state: nextState }) => nextState)
- );
-
- const widget2 = registerWidget();
- search.start();
-
- // remove widget1
- search.removeWidget(widget1);
-
- // it should have been called only once after start();
- expect(spy).toHaveBeenCalledTimes(1);
-
- // but widget2 getConfiguration() should have been called twice
- expect(widget2.getConfiguration).toHaveBeenCalledTimes(2);
- });
});
describe('When adding widgets after start', () => {
- function registerWidget(
- widgetGetConfiguration = {},
- dispose = sinon.spy()
- ) {
+ function registerWidget(widgetGetConfiguration = {}, dispose = jest.fn()) {
const widget = {
- getConfiguration: sinon.stub().returns(widgetGetConfiguration),
- init: sinon.spy(),
- render: sinon.spy(),
+ getConfiguration: jest.fn().mockReturnValue(widgetGetConfiguration),
+ init: jest.fn(),
+ render: jest.fn(),
dispose,
};
@@ -573,15 +429,14 @@ describe('InstantSearch lifecycle', () => {
beforeEach(() => {
search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: algoliasearch(appId, apiKey),
});
});
it('should add widgets after start', () => {
search.start();
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
expect(search.widgets).toHaveLength(0);
expect(search.started).toBe(true);
@@ -589,14 +444,14 @@ describe('InstantSearch lifecycle', () => {
const widget1 = registerWidget({ facets: ['price'] });
search.addWidget(widget1);
- expect(helperSearchSpy.callCount).toBe(2);
- expect(widget1.init.calledOnce).toBe(true);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(2);
+ expect(widget1.init).toHaveBeenCalledTimes(1);
const widget2 = registerWidget({ disjunctiveFacets: ['categories'] });
search.addWidget(widget2);
- expect(widget2.init.calledOnce).toBe(true);
- expect(helperSearchSpy.callCount).toBe(3);
+ expect(widget2.init).toHaveBeenCalledTimes(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(3);
expect(search.widgets).toHaveLength(2);
expect(search.searchParameters.facets).toEqual(['price']);
@@ -606,7 +461,7 @@ describe('InstantSearch lifecycle', () => {
it('should trigger only one search using `addWidgets()`', () => {
search.start();
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
expect(search.widgets).toHaveLength(0);
expect(search.started).toBe(true);
@@ -615,7 +470,7 @@ describe('InstantSearch lifecycle', () => {
search.addWidgets([widget1, widget2]);
- expect(helperSearchSpy.callCount).toBe(2);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(2);
expect(search.searchParameters.facets).toEqual(['price']);
expect(search.searchParameters.disjunctiveFacets).toEqual(['categories']);
});
@@ -623,13 +478,13 @@ describe('InstantSearch lifecycle', () => {
it('should not trigger a search without widgets to add', () => {
search.start();
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
expect(search.widgets).toHaveLength(0);
expect(search.started).toBe(true);
search.addWidgets([]);
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
expect(search.widgets).toHaveLength(0);
expect(search.started).toBe(true);
});
@@ -637,9 +492,8 @@ describe('InstantSearch lifecycle', () => {
it('should remove all widgets without triggering a search on dispose', () => {
search = new InstantSearch({
- appId,
- apiKey,
indexName,
+ searchClient: algoliasearch(appId, apiKey),
});
const widgets = times(5, () => ({
@@ -653,11 +507,54 @@ describe('InstantSearch lifecycle', () => {
search.start();
expect(search.widgets).toHaveLength(5);
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
search.dispose();
expect(search.widgets).toHaveLength(0);
- expect(helperSearchSpy.callCount).toBe(1);
+ expect(helperSearchSpy).toHaveBeenCalledTimes(1);
});
});
+
+it('Allows to start without widgets', () => {
+ const instance = new InstantSearch({
+ searchClient: {
+ search() {
+ return Promise.resolve({
+ results: [
+ {
+ query: 'fake query',
+ },
+ ],
+ });
+ },
+ },
+ indexName: 'bogus',
+ });
+
+ expect(() => instance.start()).not.toThrow();
+});
+
+it('Does not allow to start twice', () => {
+ const instance = new InstantSearch({
+ searchClient: {
+ search() {
+ return Promise.resolve({
+ results: [
+ {
+ query: 'fake query',
+ },
+ ],
+ });
+ },
+ },
+ indexName: 'bogus',
+ });
+
+ expect(() => instance.start()).not.toThrow();
+ expect(() => {
+ instance.start();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"start() has been already called once"`
+ );
+});
diff --git a/src/lib/__tests__/RoutingManager-test.js b/src/lib/__tests__/RoutingManager-test.js
index 07bd0f4154..4339cb1dd4 100644
--- a/src/lib/__tests__/RoutingManager-test.js
+++ b/src/lib/__tests__/RoutingManager-test.js
@@ -1,19 +1,18 @@
+import algoliasearch from 'algoliasearch';
import instantsearch from '../main.js';
import RoutingManager from '../RoutingManager.js';
import simpleMapping from '../stateMappings/simple.js';
-const makeFakeAlgoliaClient = () => ({
+const fakeAlgoliaClient = {
search: () => Promise.resolve({ results: [{}] }),
-});
+};
describe('RoutingManager', () => {
describe('getAllUIStates', () => {
test('reads the state of widgets with a getWidgetState implementation', () => {
const search = instantsearch({
- appId: '',
- apiKey: '',
indexName: '',
- createAlgoliaClient: makeFakeAlgoliaClient,
+ searchClient: fakeAlgoliaClient,
});
const widgetState = {
@@ -56,10 +55,8 @@ describe('RoutingManager', () => {
test('Does not read UI state from widgets without an implementation of getWidgetState', () => {
const search = instantsearch({
- appId: '',
- apiKey: '',
indexName: '',
- createAlgoliaClient: makeFakeAlgoliaClient,
+ searchClient: fakeAlgoliaClient,
});
search.addWidget({
@@ -90,10 +87,8 @@ describe('RoutingManager', () => {
describe('getAllSearchParameters', () => {
test('should get searchParameters from widget that implements getWidgetSearchParameters', () => {
const search = instantsearch({
- appId: '',
- apiKey: '',
indexName: '',
- createAlgoliaClient: makeFakeAlgoliaClient,
+ searchClient: fakeAlgoliaClient,
});
const widget = {
@@ -133,10 +128,8 @@ describe('RoutingManager', () => {
test('should not change the searchParameters if no widget has a getWidgetSearchParameters', () => {
const search = instantsearch({
- appId: '',
- apiKey: '',
indexName: '',
- createAlgoliaClient: makeFakeAlgoliaClient,
+ searchClient: fakeAlgoliaClient,
});
const widget = {
@@ -173,9 +166,11 @@ describe('RoutingManager', () => {
},
};
const search = instantsearch({
- appId: 'latency',
- apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
indexName: 'instant_search',
+ searchClient: algoliasearch(
+ 'latency',
+ '6be0576ff61c053d5f9a3225e2a90f76'
+ ),
routing: {
router,
},
@@ -221,9 +216,11 @@ describe('RoutingManager', () => {
},
};
const search = instantsearch({
- appId: 'latency',
- apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
indexName: 'instant_search',
+ searchClient: algoliasearch(
+ 'latency',
+ '6be0576ff61c053d5f9a3225e2a90f76'
+ ),
routing: {
router,
},
@@ -275,9 +272,11 @@ describe('RoutingManager', () => {
},
};
const search = instantsearch({
- appId: 'latency',
- apiKey: '6be0576ff61c053d5f9a3225e2a90f76',
indexName: 'instant_search',
+ searchClient: algoliasearch(
+ 'latency',
+ '6be0576ff61c053d5f9a3225e2a90f76'
+ ),
routing: {
router,
stateMapping,
diff --git a/src/lib/__tests__/__snapshots__/InstantSearch-test.js.snap b/src/lib/__tests__/__snapshots__/InstantSearch-test.js.snap
index 09bcd21de5..d3bd063304 100644
--- a/src/lib/__tests__/__snapshots__/InstantSearch-test.js.snap
+++ b/src/lib/__tests__/__snapshots__/InstantSearch-test.js.snap
@@ -15,11 +15,21 @@ Array [
"_lastQueryIdReceived": -1,
"_queryId": 0,
"client": Object {
- "algolia": "client",
+ "search": [Function],
},
"derivedHelpers": Array [],
"lastResults": null,
- "search": [Function],
+ "search": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ },
"state": SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -87,7 +97,7 @@ Array [
"_searchStalledTimer": null,
"_stalledSearchDelay": 200,
"client": Object {
- "algolia": "client",
+ "search": [Function],
},
"domain": null,
"helper": AlgoliaSearchHelper {
@@ -101,11 +111,21 @@ Array [
"_lastQueryIdReceived": -1,
"_queryId": 0,
"client": Object {
- "algolia": "client",
+ "search": [Function],
},
"derivedHelpers": Array [],
"lastResults": null,
- "search": [Function],
+ "search": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ },
"state": SearchParameters {
"advancedSyntax": undefined,
"allowTyposOnNumericTokens": undefined,
@@ -180,20 +200,214 @@ Array [
"compileOptions": Object {},
"helpers": Object {
"formatNumber": [Function],
+ "highlight": [Function],
+ "snippet": [Function],
},
},
- "urlSync": Object {},
"widgets": Array [
Object {
- "getConfiguration": [Function],
- "init": [Function],
- "render": [Function],
- },
- Object {
- "createURL": [Function],
- "getConfiguration": [Function],
- "onHistoryChange": [Function],
- "render": [Function],
+ "getConfiguration": [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "another": Object {
+ "config": "parameter",
+ },
+ "index": "lifecycle",
+ "some": "configuration",
+ "values": Array [
+ -2,
+ -1,
+ ],
+ },
+ undefined,
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": Object {
+ "another": Object {
+ "different": "parameter",
+ },
+ "some": "modified",
+ },
+ },
+ ],
+ },
+ "init": [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "createURL": [Function],
+ "helper": AlgoliaSearchHelper {
+ "_currentNbQueries": 0,
+ "_events": Object {
+ "error": [Function],
+ "result": [Function],
+ "search": [Function],
+ },
+ "_eventsCount": 3,
+ "_lastQueryIdReceived": -1,
+ "_queryId": 0,
+ "client": Object {
+ "search": [Function],
+ },
+ "derivedHelpers": Array [],
+ "lastResults": null,
+ "search": [MockFunction] {
+ "calls": Array [
+ Array [],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ },
+ "state": SearchParameters {
+ "advancedSyntax": undefined,
+ "allowTyposOnNumericTokens": undefined,
+ "analytics": undefined,
+ "analyticsTags": undefined,
+ "aroundLatLng": undefined,
+ "aroundLatLngViaIP": undefined,
+ "aroundPrecision": undefined,
+ "aroundRadius": undefined,
+ "attributesToHighlight": undefined,
+ "attributesToRetrieve": undefined,
+ "attributesToSnippet": undefined,
+ "disableExactOnAttributes": undefined,
+ "disjunctiveFacets": Array [],
+ "disjunctiveFacetsRefinements": Object {},
+ "distinct": undefined,
+ "enableExactOnSingleWordQuery": undefined,
+ "facets": Array [],
+ "facetsExcludes": Object {},
+ "facetsRefinements": Object {},
+ "getRankingInfo": undefined,
+ "hierarchicalFacets": Array [],
+ "hierarchicalFacetsRefinements": Object {},
+ "highlightPostTag": undefined,
+ "highlightPreTag": undefined,
+ "hitsPerPage": undefined,
+ "ignorePlurals": undefined,
+ "index": "",
+ "insideBoundingBox": undefined,
+ "insidePolygon": undefined,
+ "length": undefined,
+ "maxValuesPerFacet": undefined,
+ "minProximity": undefined,
+ "minWordSizefor1Typo": undefined,
+ "minWordSizefor2Typos": undefined,
+ "minimumAroundRadius": undefined,
+ "numericFilters": undefined,
+ "numericRefinements": Object {},
+ "offset": undefined,
+ "optionalFacetFilters": undefined,
+ "optionalTagFilters": undefined,
+ "optionalWords": undefined,
+ "page": 0,
+ "query": "",
+ "queryType": undefined,
+ "removeWordsIfNoResults": undefined,
+ "replaceSynonymsInHighlight": undefined,
+ "restrictSearchableAttributes": undefined,
+ "sendMeToUrlSync": true,
+ "snippetEllipsisText": undefined,
+ "synonyms": undefined,
+ "tagFilters": undefined,
+ "tagRefinements": Array [],
+ "typoTolerance": undefined,
+ },
+ },
+ "instantSearchInstance": [Circular],
+ "onHistoryChange": [Function],
+ "state": SearchParameters {
+ "advancedSyntax": undefined,
+ "allowTyposOnNumericTokens": undefined,
+ "analytics": undefined,
+ "analyticsTags": undefined,
+ "aroundLatLng": undefined,
+ "aroundLatLngViaIP": undefined,
+ "aroundPrecision": undefined,
+ "aroundRadius": undefined,
+ "attributesToHighlight": undefined,
+ "attributesToRetrieve": undefined,
+ "attributesToSnippet": undefined,
+ "disableExactOnAttributes": undefined,
+ "disjunctiveFacets": Array [],
+ "disjunctiveFacetsRefinements": Object {},
+ "distinct": undefined,
+ "enableExactOnSingleWordQuery": undefined,
+ "facets": Array [],
+ "facetsExcludes": Object {},
+ "facetsRefinements": Object {},
+ "getRankingInfo": undefined,
+ "hierarchicalFacets": Array [],
+ "hierarchicalFacetsRefinements": Object {},
+ "highlightPostTag": undefined,
+ "highlightPreTag": undefined,
+ "hitsPerPage": undefined,
+ "ignorePlurals": undefined,
+ "index": "",
+ "insideBoundingBox": undefined,
+ "insidePolygon": undefined,
+ "length": undefined,
+ "maxValuesPerFacet": undefined,
+ "minProximity": undefined,
+ "minWordSizefor1Typo": undefined,
+ "minWordSizefor2Typos": undefined,
+ "minimumAroundRadius": undefined,
+ "numericFilters": undefined,
+ "numericRefinements": Object {},
+ "offset": undefined,
+ "optionalFacetFilters": undefined,
+ "optionalTagFilters": undefined,
+ "optionalWords": undefined,
+ "page": 0,
+ "query": "",
+ "queryType": undefined,
+ "removeWordsIfNoResults": undefined,
+ "replaceSynonymsInHighlight": undefined,
+ "restrictSearchableAttributes": undefined,
+ "sendMeToUrlSync": true,
+ "snippetEllipsisText": undefined,
+ "synonyms": undefined,
+ "tagFilters": undefined,
+ "tagRefinements": Array [],
+ "typoTolerance": undefined,
+ },
+ "templatesConfig": Object {
+ "compileOptions": Object {},
+ "helpers": Object {
+ "formatNumber": [Function],
+ "highlight": [Function],
+ "snippet": [Function],
+ },
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ },
+ "render": [MockFunction] {
+ "calls": Array [
+ [Circular],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ },
},
],
},
@@ -262,6 +476,8 @@ Array [
"compileOptions": Object {},
"helpers": Object {
"formatNumber": [Function],
+ "highlight": [Function],
+ "snippet": [Function],
},
},
},
diff --git a/src/lib/__tests__/__snapshots__/search-client-test.js.snap b/src/lib/__tests__/__snapshots__/search-client-test.js.snap
index caf480712a..4b04d6f6b0 100644
--- a/src/lib/__tests__/__snapshots__/search-client-test.js.snap
+++ b/src/lib/__tests__/__snapshots__/search-client-test.js.snap
@@ -28,4 +28,4 @@ Array [
]
`;
-exports[`InstantSearch Search Client Properties throws if no \`search()\` method 1`] = `"InstantSearch configuration error: \`searchClient\` must implement a \`search(requests)\` method."`;
+exports[`InstantSearch Search Client Properties throws if no \`search()\` method 1`] = `"The search client must implement a \`search(requests)\` method."`;
diff --git a/src/lib/__tests__/__snapshots__/url-sync-test.js.snap b/src/lib/__tests__/__snapshots__/url-sync-test.js.snap
deleted file mode 100644
index e8ef4d9561..0000000000
--- a/src/lib/__tests__/__snapshots__/url-sync-test.js.snap
+++ /dev/null
@@ -1,5 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`urlSync mechanics Generates urls on change 1`] = `"q=query&idx=&p=0"`;
-
-exports[`urlSync mechanics updates the URL during the first rendering if it has change since the initial configuration 1`] = `"q=query&idx=&p=0"`;
diff --git a/src/lib/__tests__/api-collision.js b/src/lib/__tests__/api-collision.js
deleted file mode 100644
index 960afdbe1e..0000000000
--- a/src/lib/__tests__/api-collision.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* eslint no-new: off */
-import InstantSearch from '../InstantSearch';
-
-const usage = `
-Usage: instantsearch({
- indexName: 'my_index_name',
- searchClient: algoliasearch('appId', 'apiKey')
-});`;
-
-// THROWAWAY: Test suite to remove once the next major version is released
-describe('InstantSearch API collision', () => {
- describe('with search client', () => {
- const appId = 'appId';
- const apiKey = 'apiKey';
- const indexName = 'indexName';
- const searchClient = { search() {} };
-
- it('and indexName', () => {
- expect(() => {
- new InstantSearch({
- indexName,
- searchClient,
- });
- }).not.toThrow();
- });
-
- it('and nothing else', () => {
- expect(() => {
- new InstantSearch({
- searchClient,
- });
- }).toThrow(usage);
- });
-
- it('and appId', () => {
- expect(() => {
- new InstantSearch({
- appId,
- searchClient,
- });
- }).toThrow(usage);
- });
-
- it('and apiKey', () => {
- expect(() => {
- new InstantSearch({
- apiKey,
- searchClient,
- });
- }).toThrow(usage);
- });
-
- it('and createAlgoliaClient', () => {
- expect(() => {
- new InstantSearch({
- createAlgoliaClient: () => {},
- searchClient,
- });
- }).toThrow(usage);
- });
- });
-});
diff --git a/src/lib/__tests__/escape-highlight-test.js b/src/lib/__tests__/escape-highlight-test.js
index 090c62972a..fa52ace5c2 100644
--- a/src/lib/__tests__/escape-highlight-test.js
+++ b/src/lib/__tests__/escape-highlight-test.js
@@ -21,12 +21,12 @@ describe('escapeHits()', () => {
{
_highlightResult: {
foobar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
_snippetResult: {
foobar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
},
@@ -63,14 +63,14 @@ describe('escapeHits()', () => {
_highlightResult: {
foo: {
bar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
},
_snippetResult: {
foo: {
bar: {
- value: '<script>foobar </script>',
+ value: '<script>foobar </script>',
},
},
},
@@ -111,14 +111,14 @@ describe('escapeHits()', () => {
{
_highlightResult: {
foobar: [
- { value: '<script>bar </script>' },
- { value: '<script>foo </script>' },
+ { value: '<script>bar </script>' },
+ { value: '<script>foo </script>' },
],
},
_snippetResult: {
foobar: [
- { value: '<script>bar </script>' },
- { value: '<script>foo </script>' },
+ { value: '<script>bar </script>' },
+ { value: '<script>foo </script>' },
],
},
},
@@ -181,14 +181,14 @@ describe('escapeHits()', () => {
{
foo: {
bar: {
- value: '<script>bar </script>',
+ value: '<script>bar </script>',
},
},
},
{
foo: {
bar: {
- value: '<script>foo </script>',
+ value: '<script>foo </script>',
},
},
},
@@ -199,14 +199,14 @@ describe('escapeHits()', () => {
{
foo: {
bar: {
- value: '<script>bar </script>',
+ value: '<script>bar </script>',
},
},
},
{
foo: {
bar: {
- value: '<script>foo </script>',
+ value: '<script>foo </script>',
},
},
},
@@ -236,7 +236,7 @@ describe('escapeHits()', () => {
{
_highlightResult: {
foobar: {
- value: '<script>foo </script>',
+ value: '<script>foo </script>',
},
},
},
diff --git a/src/lib/__tests__/main-test.js b/src/lib/__tests__/main-test.js
index 424ed2348d..63e79372de 100644
--- a/src/lib/__tests__/main-test.js
+++ b/src/lib/__tests__/main-test.js
@@ -1,6 +1,5 @@
import instantsearch from '../main.js';
import forEach from 'lodash/forEach';
-import expect from 'expect';
describe('instantsearch()', () => {
// to ensure the global.window is set
@@ -11,21 +10,6 @@ describe('instantsearch()', () => {
);
});
- it('statically creates a URL', () => {
- expect(instantsearch.createQueryString({ hitsPerPage: 42 })).toEqual(
- 'hPP=42'
- );
- });
-
- it('statically creates a complex URL', () => {
- expect(
- instantsearch.createQueryString({
- hitsPerPage: 42,
- facetsRefinements: { category: 'Home' },
- })
- ).toEqual('hPP=42&fR[category]=Home');
- });
-
it('includes the widget functions', () => {
forEach(instantsearch.widgets, widget => {
expect(typeof widget).toEqual('function', 'A widget must be a function');
@@ -40,4 +24,11 @@ describe('instantsearch()', () => {
);
});
});
+
+ it('includes the highlight helper function', () => {
+ expect(typeof instantsearch.highlight).toEqual(
+ 'function',
+ 'THe highlight helper must be a function'
+ );
+ });
});
diff --git a/src/lib/__tests__/suits-test.js b/src/lib/__tests__/suits-test.js
new file mode 100644
index 0000000000..70b1b954fe
--- /dev/null
+++ b/src/lib/__tests__/suits-test.js
@@ -0,0 +1,24 @@
+import { component } from '../suit.js';
+
+describe('suit - component classname generation', () => {
+ test('generates a name with the component name, modifier and descendant', () => {
+ expect(
+ component('MyComponent')({
+ modifierName: 'mod',
+ descendantName: 'desc',
+ })
+ ).toEqual('ais-MyComponent-desc--mod');
+ });
+
+ test('generates a name with the component name and descendant', () => {
+ expect(component('MyComponent')({ descendantName: 'desc' })).toEqual(
+ 'ais-MyComponent-desc'
+ );
+ });
+
+ test('generates a name with the component name and modifier', () => {
+ expect(component('MyComponent')({ modifierName: 'mod' })).toEqual(
+ 'ais-MyComponent--mod'
+ );
+ });
+});
diff --git a/src/lib/__tests__/url-sync-test.js b/src/lib/__tests__/url-sync-test.js
deleted file mode 100644
index d6a7faa2a9..0000000000
--- a/src/lib/__tests__/url-sync-test.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import urlSync from '../url-sync.js';
-import jsHelper from 'algoliasearch-helper';
-const SearchParameters = jsHelper.SearchParameters;
-
-jest.useFakeTimers();
-
-const makeTestUrlUtils = () => ({
- url: '',
- lastQs: '',
- onpopstate(/* cb */) {
- // window.addEventListener('popstate', cb);
- },
- pushState(qs /* , {getHistoryState} */) {
- this.lastQs = qs;
- // window.history.pushState(getHistoryState(), '', getFullURL(this.createURL(qs)));
- },
- createURL(qs) {
- return qs;
- },
- readUrl() {
- // return window.location.search.slice(1);
- return this.url;
- },
-});
-
-describe('urlSync mechanics', () => {
- test('Generates urls on change', () => {
- const helper = jsHelper({});
- const urlUtils = makeTestUrlUtils();
- const urlSyncWidget = urlSync({ urlUtils, threshold: 0 });
- urlSyncWidget.init({ state: SearchParameters.make({}) });
- urlSyncWidget.render({ helper, state: helper.state });
-
- expect(urlUtils.lastQs).toEqual('');
- helper.setQuery('query');
- expect(urlUtils.lastQs).toEqual('');
-
- jest.runOnlyPendingTimers();
-
- expect(urlUtils.lastQs).toMatchSnapshot();
- });
- test('Generated urls should not contain a version', () => {
- const helper = jsHelper({});
- const urlUtils = makeTestUrlUtils();
- const urlSyncWidget = urlSync({ urlUtils, threshold: 0 });
- urlSyncWidget.init({ state: SearchParameters.make({}) });
- urlSyncWidget.render({ helper, state: helper.state });
- helper.setQuery('query');
-
- jest.runOnlyPendingTimers();
-
- expect(urlUtils.lastQs).not.toEqual(expect.stringContaining('is_v'));
- });
- test('updates the URL during the first rendering if it has change since the initial configuration', () => {
- const helper = jsHelper({});
- const urlUtils = makeTestUrlUtils();
- const urlSyncWidget = urlSync({ urlUtils, threshold: 0 });
- urlSyncWidget.init({ state: SearchParameters.make({}) });
-
- // In this scenario, there should have been a search here
- // but it was prevented by a search function
- helper.setQuery('query');
- // the change even is setup at the first rendering
- urlSyncWidget.render({ helper, state: helper.state });
-
- // because the state has changed before the first rendering,
- // we expect the URL to be updated
- jest.runOnlyPendingTimers();
- expect(urlUtils.lastQs).toMatchSnapshot();
- });
-});
diff --git a/src/lib/__tests__/utils-test.js b/src/lib/__tests__/utils-test.js
index d6f1676cb5..ca2cf409b9 100644
--- a/src/lib/__tests__/utils-test.js
+++ b/src/lib/__tests__/utils-test.js
@@ -1,6 +1,12 @@
import algoliasearchHelper from 'algoliasearch-helper';
import * as utils from '../utils';
+describe('capitalize', () => {
+ it('should capitalize the first character only', () => {
+ expect(utils.capitalize('hello')).toBe('Hello');
+ });
+});
+
describe('utils.getContainerNode', () => {
it('should be able to get a node from a node', () => {
const d = document.body;
@@ -80,17 +86,14 @@ describe('utils.prepareTemplateProps', () => {
bar: 'tata',
};
const templatesConfig = [];
- const transformData = () => {}; // eslint-disable-line func-style
it('should return the default templates and set useCustomCompileOptions to false when using the defaults', () => {
const defaultsPrepared = utils.prepareTemplateProps({
- transformData,
defaultTemplates,
undefined,
templatesConfig,
});
- expect(defaultsPrepared.transformData).toBe(transformData);
expect(defaultsPrepared.useCustomCompileOptions).toEqual({
foo: false,
bar: false,
@@ -102,13 +105,11 @@ describe('utils.prepareTemplateProps', () => {
it('should return the missing default templates and set useCustomCompileOptions for the custom template', () => {
const templates = { foo: 'baz' };
const defaultsPrepared = utils.prepareTemplateProps({
- transformData,
defaultTemplates,
templates,
templatesConfig,
});
- expect(defaultsPrepared.transformData).toBe(transformData);
expect(defaultsPrepared.useCustomCompileOptions).toEqual({
foo: true,
bar: false,
@@ -127,13 +128,11 @@ describe('utils.prepareTemplateProps', () => {
};
const preparedProps = utils.prepareTemplateProps({
- transformData,
defaultTemplates,
templates,
templatesConfig,
});
- expect(preparedProps.transformData).toBe(transformData);
expect(preparedProps.useCustomCompileOptions).toEqual({
foo: true,
bar: false,
@@ -198,6 +197,18 @@ describe('utils.renderTemplate', () => {
expect(actual).toBe(expectation);
});
+ it('expect to compress templates', () => {
+ expect(
+ utils.renderTemplate({
+ templateKey: 'message',
+ templates: {
+ message: ` hello
+ message
`,
+ },
+ })
+ ).toMatchInlineSnapshot(`" hello message
"`);
+ });
+
it('expect to throw when the template is not a function or a string', () => {
const actual0 = () =>
utils.renderTemplate({
@@ -342,24 +353,24 @@ describe('utils.getRefinements', () => {
);
});
- it('should inject query facet if clearQuery === true', () => {
- helper.setQuery('my query');
+ it('should retrieve one query refinement when `clearsQuery` is true', () => {
+ helper.setQuery('a query');
const expected = [
{
type: 'query',
attributeName: 'query',
- name: 'my query',
- query: 'my query',
+ name: 'a query',
+ query: 'a query',
},
];
const clearsQuery = true;
- expect(utils.getRefinements(results, helper.state, clearsQuery)).toEqual(
- expected
- );
+ expect(
+ utils.getRefinements(results, helper.state, clearsQuery)
+ ).toContainEqual(expected[0]);
});
- it('should retrieve one facetRefinement and not inject query facet if clearQuery === false', () => {
- helper.setQuery('my query');
+ it('should not retrieve any query refinements if `clearsQuery` if false', () => {
+ helper.setQuery('a query');
const expected = [];
const clearsQuery = false;
expect(utils.getRefinements(results, helper.state, clearsQuery)).toEqual(
@@ -706,9 +717,10 @@ describe('utils.getRefinements', () => {
{
type: 'hierarchical',
attributeName: 'hierarchicalFacet2',
- name: 'lvl1val1',
+ name: 'hierarchicalFacet2lvl0val1 > lvl1val1',
},
];
+
expect(utils.getRefinements(results, helper.state)).toContainEqual(
expected[0]
);
@@ -956,26 +968,91 @@ describe('utils.warn', () => {
});
});
-describe('utils.parseAroundLatLngFromString', () => {
- it('expect to return a LatLng object from string', () => {
- const samples = [
- { input: '10,12', expectation: { lat: 10, lng: 12 } },
- { input: '10, 12', expectation: { lat: 10, lng: 12 } },
- { input: '10.15,12', expectation: { lat: 10.15, lng: 12 } },
- { input: '10,12.15', expectation: { lat: 10, lng: 12.15 } },
- ];
-
- samples.forEach(({ input, expectation }) => {
- expect(utils.parseAroundLatLngFromString(input)).toEqual(expectation);
- });
+describe('utils.aroundLatLngToPosition', () => {
+ it.each([
+ ['10,12', { lat: 10, lng: 12 }],
+ ['10, 12', { lat: 10, lng: 12 }],
+ ['10.15,12', { lat: 10.15, lng: 12 }],
+ ['10,12.15', { lat: 10, lng: 12.15 }],
+ ])('expect to return a Position from a string: %j', (input, expectation) => {
+ expect(utils.aroundLatLngToPosition(input)).toEqual(expectation);
});
- it('expect to throw an error when the parsing fail', () => {
- const samples = [{ input: '10a,12' }, { input: '10. 12' }];
+ it.each([['10a,12'], ['10. 12']])(
+ 'expect to throw an error with: %j',
+ input => {
+ expect(() => utils.aroundLatLngToPosition(input)).toThrowError(
+ `Invalid value for "aroundLatLng" parameter: "${input}"`
+ );
+ }
+ );
+});
- samples.forEach(({ input }) => {
- expect(() => utils.parseAroundLatLngFromString(input)).toThrow();
- });
+describe('utils.insideBoundingBoxToBoundingBox', () => {
+ it.each([
+ [
+ '10,12,12,14',
+ {
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ },
+ ],
+ [
+ '10, 12 ,12 , 14',
+ {
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ },
+ ],
+ [
+ '10.15,12.15,12.15,14.15',
+ {
+ northEast: { lat: 10.15, lng: 12.15 },
+ southWest: { lat: 12.15, lng: 14.15 },
+ },
+ ],
+ ])(
+ 'expect to return a BoundingBox from a string: %j',
+ (input, expectation) => {
+ expect(utils.insideBoundingBoxToBoundingBox(input)).toEqual(expectation);
+ }
+ );
+
+ it.each([
+ [
+ [[10, 12, 12, 14]],
+ {
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ },
+ ],
+ [
+ [[10.15, 12.15, 12.15, 14.15]],
+ {
+ northEast: { lat: 10.15, lng: 12.15 },
+ southWest: { lat: 12.15, lng: 14.15 },
+ },
+ ],
+ ])(
+ 'expect to return a BoundingBox from an array: %j',
+ (input, expectation) => {
+ expect(utils.insideBoundingBoxToBoundingBox(input)).toEqual(expectation);
+ }
+ );
+
+ it.each([[''], ['10'], ['10,12'], ['10,12,12'], ['10. 15,12,12']])(
+ 'expect to throw an error with: %j',
+ input => {
+ expect(() => utils.insideBoundingBoxToBoundingBox(input)).toThrowError(
+ `Invalid value for "insideBoundingBox" parameter: "${input}"`
+ );
+ }
+ );
+
+ it.each([[[]], [[[]]]])('expect to throw an error with: %j', input => {
+ expect(() => utils.insideBoundingBoxToBoundingBox(input)).toThrowError(
+ `Invalid value for "insideBoundingBox" parameter: [${input}]`
+ );
});
});
@@ -995,201 +1072,124 @@ describe('utils.clearRefinements', () => {
return helper;
};
- describe('Without clearsQuery', () => {
- it('can clear all the parameters refined', () => {
- const helper = initHelperWithRefinements();
-
- const finalState = utils.clearRefinements({
- helper,
- });
-
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual([]);
- });
-
- it('can clear all the parameters defined in the whiteList', () => {
- const helper = initHelperWithRefinements();
-
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['conjFacet'],
- });
-
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual(
- helper.state.disjunctiveFacetsRefinements
- );
- expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
- });
-
- it('can clear all the parameters refined but the ones in the black list', () => {
- const helper = initHelperWithRefinements();
-
- const finalState = utils.clearRefinements({
- helper,
- blackList: ['conjFacet'],
- });
+ it('does not clear anything without attributes', () => {
+ const helper = initHelperWithRefinements();
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual([]);
+ const finalState = utils.clearRefinements({
+ helper,
});
- it('can clear all the parameters in the whitelist except the ones in the black list', () => {
- const helper = initHelperWithRefinements();
-
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['conjFacet', 'disjFacet'],
- blackList: ['conjFacet'],
- });
-
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual(finalState.tagRefinements);
- });
-
- it('can clear tags only (whitelisting tags)', () => {
- const helper = initHelperWithRefinements();
+ expect(finalState.query).toBe(helper.state.query);
+ expect(finalState.facetsRefinements).toEqual(
+ helper.state.facetsRefinements
+ );
+ expect(finalState.disjunctiveFacetsRefinements).toEqual(
+ helper.state.disjunctiveFacetsRefinements
+ );
+ expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
+ });
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['_tags'],
- });
+ it('can clear all the parameters defined in the list', () => {
+ const helper = initHelperWithRefinements();
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual(
- finalState.disjunctiveFacetsRefinements
- );
- expect(finalState.tagRefinements).toEqual([]);
+ const finalState = utils.clearRefinements({
+ helper,
+ attributesToClear: ['conjFacet'],
});
- it('can clear everything but the tags (blacklisting tags)', () => {
- const helper = initHelperWithRefinements();
-
- const finalState = utils.clearRefinements({
- helper,
- blackList: ['_tags'],
- });
-
- expect(finalState.query).toBe(helper.state.query);
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual(finalState.tagRefinements);
- });
+ expect(finalState.query).toBe(helper.state.query);
+ expect(finalState.facetsRefinements).toEqual({});
+ expect(finalState.disjunctiveFacetsRefinements).toEqual(
+ helper.state.disjunctiveFacetsRefinements
+ );
+ expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
});
- describe('With clearsQuery', () => {
- it('can clear all the parameters refined', () => {
- const helper = initHelperWithRefinements();
+ it('can clear tags only (including tags)', () => {
+ const helper = initHelperWithRefinements();
- const finalState = utils.clearRefinements({
- helper,
- clearsQuery: true,
- });
-
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual([]);
+ const finalState = utils.clearRefinements({
+ helper,
+ attributesToClear: ['_tags'],
});
- it('can clear all the parameters defined in the whiteList', () => {
- const helper = initHelperWithRefinements();
+ expect(finalState.query).toBe(helper.state.query);
+ expect(finalState.facetsRefinements).toEqual(
+ helper.state.facetsRefinements
+ );
+ expect(finalState.disjunctiveFacetsRefinements).toEqual(
+ finalState.disjunctiveFacetsRefinements
+ );
+ expect(finalState.tagRefinements).toEqual([]);
+ });
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['conjFacet'],
- clearsQuery: true,
- });
+ it('can clear the query alone', () => {
+ const helper = initHelperWithRefinements();
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual(
- helper.state.disjunctiveFacetsRefinements
- );
- expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
+ const finalState = utils.clearRefinements({
+ helper,
+ attributesToClear: ['query'],
});
- it('can clear all the parameters refined but the ones in the black list', () => {
- const helper = initHelperWithRefinements();
+ expect(finalState.query).toBe('');
+ expect(finalState.facetsRefinements).toEqual(
+ helper.state.facetsRefinements
+ );
+ expect(finalState.disjunctiveFacetsRefinements).toEqual(
+ helper.state.disjunctiveFacetsRefinements
+ );
+ expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
+ });
- const finalState = utils.clearRefinements({
- helper,
- blackList: ['conjFacet'],
- clearsQuery: true,
- });
+ it('can clear the query alone and other refinements', () => {
+ const helper = initHelperWithRefinements();
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual([]);
+ const finalState = utils.clearRefinements({
+ helper,
+ attributesToClear: ['query', 'conjFacet'],
});
- it('can clear all the parameters in the whitelist except the ones in the black list', () => {
- const helper = initHelperWithRefinements();
+ expect(finalState.query).toBe('');
+ expect(finalState.facetsRefinements).toEqual({});
+ expect(finalState.disjunctiveFacetsRefinements).toEqual(
+ helper.state.disjunctiveFacetsRefinements
+ );
+ expect(finalState.tagRefinements).toEqual(helper.state.tagRefinements);
+ });
+});
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['conjFacet', 'disjFacet'],
- blackList: ['conjFacet'],
- clearsQuery: true,
- });
+describe('utils.getPropertyByPath', () => {
+ it('should be able to get a property', () => {
+ const object = {
+ name: 'name',
+ };
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual(finalState.tagRefinements);
- });
+ expect(utils.getPropertyByPath(object, 'name')).toBe('name');
+ });
- it('can clear tags only (whitelisting tags)', () => {
- const helper = initHelperWithRefinements();
+ it('should be able to get a nested property', () => {
+ const object = {
+ nested: {
+ name: 'name',
+ },
+ };
- const finalState = utils.clearRefinements({
- helper,
- whiteList: ['_tags'],
- clearsQuery: true,
- });
+ expect(utils.getPropertyByPath(object, 'nested.name')).toBe('name');
+ });
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual(
- helper.state.facetsRefinements
- );
- expect(finalState.disjunctiveFacetsRefinements).toEqual(
- finalState.disjunctiveFacetsRefinements
- );
- expect(finalState.tagRefinements).toEqual([]);
- });
+ it('returns undefined if does not exist', () => {
+ const object = {};
- it('can clear everything but the tags (blacklisting tags)', () => {
- const helper = initHelperWithRefinements();
+ expect(utils.getPropertyByPath(object, 'random')).toBe(undefined);
+ });
- const finalState = utils.clearRefinements({
- helper,
- blackList: ['_tags'],
- clearsQuery: true,
- });
+ it('should stop traversing when property is not an object', () => {
+ const object = {
+ nested: {
+ names: ['name'],
+ },
+ };
- expect(finalState.query).toBe('');
- expect(finalState.facetsRefinements).toEqual({});
- expect(finalState.disjunctiveFacetsRefinements).toEqual({});
- expect(finalState.tagRefinements).toEqual(finalState.tagRefinements);
- });
+ expect(utils.getPropertyByPath(object, 'nested.name')).toBe(undefined);
});
});
diff --git a/src/lib/__tests__/version-test.js b/src/lib/__tests__/version-test.js
index 9787f1dade..d21bf4b98e 100644
--- a/src/lib/__tests__/version-test.js
+++ b/src/lib/__tests__/version-test.js
@@ -1,4 +1,3 @@
-import expect from 'expect';
import version from '../version';
describe('version', () => {
diff --git a/src/lib/createHelpers.js b/src/lib/createHelpers.js
index 088b1c05b7..b3b4faef33 100644
--- a/src/lib/createHelpers.js
+++ b/src/lib/createHelpers.js
@@ -1,7 +1,41 @@
+import { highlight, snippet } from '../helpers';
+
export default function({ numberLocale }) {
return {
formatNumber(number, render) {
return Number(render(number)).toLocaleString(numberLocale);
},
+ highlight(options, render) {
+ try {
+ const highlightOptions = JSON.parse(options);
+
+ return render(
+ highlight({
+ ...highlightOptions,
+ hit: this,
+ })
+ );
+ } catch (error) {
+ throw new Error(`
+The highlight helper expects a JSON object of the format:
+{ "attribute": "name", "highlightedTagName": "mark" }`);
+ }
+ },
+ snippet(options, render) {
+ try {
+ const snippetOptions = JSON.parse(options);
+
+ return render(
+ snippet({
+ ...snippetOptions,
+ hit: this,
+ })
+ );
+ } catch (error) {
+ throw new Error(`
+The snippet helper expects a JSON object of the format:
+{ "attribute": "name", "highlightedTagName": "mark" }`);
+ }
+ },
};
}
diff --git a/src/lib/escape-highlight.js b/src/lib/escape-highlight.js
index decc9daa0c..e25b5b0a17 100644
--- a/src/lib/escape-highlight.js
+++ b/src/lib/escape-highlight.js
@@ -3,15 +3,26 @@ import escape from 'lodash/escape';
import isArray from 'lodash/isArray';
import isPlainObject from 'lodash/isPlainObject';
-export const tagConfig = {
+export const TAG_PLACEHOLDER = {
highlightPreTag: '__ais-highlight__',
highlightPostTag: '__/ais-highlight__',
};
-function replaceWithEmAndEscape(value) {
+export const TAG_REPLACEMENT = {
+ highlightPreTag: '',
+ highlightPostTag: ' ',
+};
+
+function replaceTagsAndEscape(value) {
return escape(value)
- .replace(new RegExp(tagConfig.highlightPreTag, 'g'), '')
- .replace(new RegExp(tagConfig.highlightPostTag, 'g'), ' ');
+ .replace(
+ new RegExp(TAG_PLACEHOLDER.highlightPreTag, 'g'),
+ TAG_REPLACEMENT.highlightPreTag
+ )
+ .replace(
+ new RegExp(TAG_PLACEHOLDER.highlightPostTag, 'g'),
+ TAG_REPLACEMENT.highlightPostTag
+ );
}
function recursiveEscape(input) {
@@ -32,7 +43,7 @@ function recursiveEscape(input) {
return {
...input,
- value: replaceWithEmAndEscape(input.value),
+ value: replaceTagsAndEscape(input.value),
};
}
@@ -58,6 +69,6 @@ export default function escapeHits(hits) {
export function escapeFacets(facetHits) {
return facetHits.map(h => ({
...h,
- highlighted: replaceWithEmAndEscape(h.highlighted),
+ highlighted: replaceTagsAndEscape(h.highlighted),
}));
}
diff --git a/src/lib/main.js b/src/lib/main.js
index 6581a0ae98..90a6ef99d6 100644
--- a/src/lib/main.js
+++ b/src/lib/main.js
@@ -1,13 +1,13 @@
/** @module module:instantsearch */
import toFactory from 'to-factory';
-import algoliasearchHelper from 'algoliasearch-helper';
import InstantSearch from './InstantSearch.js';
import version from './version.js';
import * as connectors from '../connectors/index.js';
import * as widgets from '../widgets/index.js';
+import * as helpers from '../helpers/index.js';
import * as routers from './routers/index.js';
import * as stateMappings from './stateMappings/index.js';
@@ -22,27 +22,6 @@ import * as stateMappings from './stateMappings/index.js';
* @see /instantsearch.html
*/
-/**
- * @typedef {Object} UrlSyncOptions
- * @property {Object} [mapping] Object used to define replacement query
- * parameter to use in place of another. Keys are current query parameters
- * and value the new value, e.g. `{ q: 'query' }`.
- * @property {number} [threshold=700] Idle time in ms after which a new
- * state is created in the browser history. The URL is always updated at each keystroke
- * but we only create a "previous search state" (activated when click on back button) every 700ms of idle time.
- * @property {string[]} [trackedParameters] Parameters that will
- * be synchronized in the URL. Default value is `['query', 'attribute:*',
- * 'index', 'page', 'hitsPerPage']`. `attribute:*` means all the faceting attributes will be tracked. You
- * can track only some of them by using `[..., 'attribute:color', 'attribute:categories']`. All other possible
- * values are all the [attributes of the Helper SearchParameters](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchparameters).
- * @property {boolean} [useHash] If set to `true`, the URL will be
- * hash based. Otherwise, it'll use the query parameters using the modern
- * history API.
- * @property {function} [getHistoryState] Pass this function to override the
- * default history API state we set to `null`. For example, this could be used to force passing
- * `{turbolinks: true}` to the history API every time we update it.
- */
-
/**
* @typedef {Object|boolean} RoutingOptions
* @property {Router} [router=HistoryRouter()] The router is the part that will save the UI State.
@@ -87,50 +66,12 @@ import * as stateMappings from './stateMappings/index.js';
/**
* @typedef {Object} InstantSearchOptions
- * @property {string} appId The Algolia application ID
- * @property {string} apiKey The Algolia search-only API key
* @property {string} indexName The name of the main index
- * @property {string} [numberLocale] The locale used to display numbers. This will be passed
- * to [`Number.prototype.toLocaleString()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString)
- * @property {function} [searchFunction] A hook that will be called each time a search needs to be done, with the
- * helper as a parameter. It's your responsibility to call `helper.search()`. This option allows you to avoid doing
- * searches at page load for example.
- * @property {function} [createAlgoliaClient] _Deprecated in favor of [`searchClient`](instantsearch.html#struct-InstantSearchOptions-searchClient)._
- *
- * Allows you to provide your own algolia client instead of the one instantiated internally by instantsearch.js.
- * Useful in situations where you need to setup complex mechanism on the client or if you need to share it easily.
- *
- * Usage:
- * ```javascript
- * instantsearch({
- * // other parameters
- * createAlgoliaClient: function(algoliasearch, appId, apiKey) {
- * return anyCustomClient;
- * }
- * });
- * ```
- * We forward `algoliasearch`, which is the original [Algolia search client](https://www.algolia.com/doc/api-client/javascript/getting-started) imported inside InstantSearch.js
- * @property {object} [searchParameters] Additional parameters to pass to
- * the Algolia API ([see full documentation](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchparameters)).
- * @property {boolean|UrlSyncOptions} [urlSync] _Deprecated in favor of [`routing`](instantsearch.html#struct-InstantSearchOptions-routing)._
- *
- * URL synchronization configuration.
- * Setting to `true` will synchronize the needed search parameters with the browser URL.
- * @property {number} [stalledSearchDelay=200] Time before a search is considered stalled.
- * @property {RoutingOptions} [routing] Router configuration used to save the UI State into the URL or
- * any client side persistence.
- * @property {SearchClient} [searchClient] The search client to plug to InstantSearch.js. You should start updating with this
- * syntax to ease the [migration to InstantSearch 3](./guides/prepare-for-v3.html).
+ * @property {SearchClient} searchClient The search client to plug to InstantSearch.js
*
* Usage:
* ```javascript
- * // Using the default Algolia client (https://github.com/algolia/algoliasearch-client-javascript)
- * // This is the default client used by InstantSearch. Equivalent to:
- * // instantsearch({
- * // appId: 'appId',
- * // apiKey: 'apiKey',
- * // indexName: 'indexName',
- * // });
+ * // Using the default Algolia search client
* instantsearch({
* indexName: 'indexName',
* searchClient: algoliasearch('appId', 'apiKey')
@@ -151,21 +92,32 @@ import * as stateMappings from './stateMappings/index.js';
* }
* });
* ```
+ * @property {string} [numberLocale] The locale used to display numbers. This will be passed
+ * to [`Number.prototype.toLocaleString()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString)
+ * @property {function} [searchFunction] A hook that will be called each time a search needs to be done, with the
+ * helper as a parameter. It's your responsibility to call `helper.search()`. This option allows you to avoid doing
+ * searches at page load for example.
+ * @property {object} [searchParameters] Additional parameters to pass to
+ * the Algolia API ([see full documentation](https://community.algolia.com/algoliasearch-helper-js/reference.html#searchparameters)).
+ * @property {number} [stalledSearchDelay=200] Time before a search is considered stalled.
+ * @property {RoutingOptions} [routing] Router configuration used to save the UI State into the URL or
+ * any client side persistence.
*/
/**
* InstantSearch is the main component of InstantSearch.js. This object
* manages the widget and lets you add new ones.
*
- * Three parameters are required to get you started with InstantSearch.js:
- * - `appId`: your algolia application id
- * - `apiKey`: the search key associated with your application
+ * Two parameters are required to get you started with InstantSearch.js:
* - `indexName`: the main index that you will use for your new search UI
+ * - `searchClient`: the search client to plug to InstantSearch.js
+ *
+ * The [search client provided by Algolia](https://github.com/algolia/algoliasearch-client-javascript)
+ * needs an `appId` and an `apiKey`. Those parameters can be found in your
+ * [Algolia dashboard](https://www.algolia.com/api-keys).
*
- * Those parameters can be found in your [Algolia dashboard](https://www.algolia.com/api-keys).
* If you want to get up and running quickly with InstantSearch.js, have a
* look at the [getting started](getting-started.html).
- *
* @function instantsearch
* @param {InstantSearchOptions} $0 The options
* @return {InstantSearch} the instantsearch instance
@@ -174,10 +126,10 @@ const instantsearch = toFactory(InstantSearch);
instantsearch.routers = routers;
instantsearch.stateMappings = stateMappings;
-instantsearch.createQueryString =
- algoliasearchHelper.url.getQueryStringFromState;
instantsearch.connectors = connectors;
instantsearch.widgets = widgets;
instantsearch.version = version;
+instantsearch.highlight = helpers.highlight;
+instantsearch.snippet = helpers.snippet;
export default instantsearch;
diff --git a/src/lib/suit.js b/src/lib/suit.js
new file mode 100644
index 0000000000..39b149e498
--- /dev/null
+++ b/src/lib/suit.js
@@ -0,0 +1,10 @@
+const NAMESPACE = 'ais';
+
+export const component = componentName => ({
+ modifierName,
+ descendantName,
+} = {}) => {
+ const d = descendantName ? `-${descendantName}` : '';
+ const m = modifierName ? `--${modifierName}` : '';
+ return `${NAMESPACE}-${componentName}${d}${m}`;
+};
diff --git a/src/lib/url-sync.js b/src/lib/url-sync.js
deleted file mode 100644
index 1e22349f6f..0000000000
--- a/src/lib/url-sync.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import algoliasearchHelper from 'algoliasearch-helper';
-import urlHelper from 'algoliasearch-helper/src/url';
-import isEqual from 'lodash/isEqual';
-
-const AlgoliaSearchHelper = algoliasearchHelper.AlgoliaSearchHelper;
-
-/**
- * @typedef {object} UrlUtil
- * @property {string} character the character used in the url
- * @property {function} onpopstate add an event listener for the URL change
- * @property {function} pushState creates a new entry in the browser history
- * @property {function} readUrl reads the query string of the parameters
- */
-
-/**
- * Handles the legacy browsers
- * @type {UrlUtil}
- */
-const hashUrlUtils = {
- ignoreNextPopState: false,
- character: '#',
- onpopstate(cb) {
- this._onHashChange = hash => {
- if (this.ignoreNextPopState) {
- this.ignoreNextPopState = false;
- return;
- }
-
- cb(hash);
- };
-
- window.addEventListener('hashchange', this._onHashChange);
- },
- pushState(qs) {
- // hash change or location assign does trigger an hashchange event
- // so every time we change it manually, we inform the code
- // to ignore the next hashchange event
- // see https://github.com/algolia/instantsearch.js/issues/2012
- this.ignoreNextPopState = true;
- window.location.assign(getFullURL(this.createURL(qs)));
- },
- createURL(qs) {
- return window.location.search + this.character + qs;
- },
- readUrl() {
- return window.location.hash.slice(1);
- },
- dispose() {
- window.removeEventListener('hashchange', this._onHashChange);
- window.location.assign(getFullURL(''));
- },
-};
-
-/**
- * Handles the modern API
- * @type {UrlUtil}
- */
-const modernUrlUtils = {
- character: '?',
- onpopstate(cb) {
- this._onPopState = (...args) => cb(...args);
- window.addEventListener('popstate', this._onPopState);
- },
- pushState(qs, { getHistoryState }) {
- window.history.pushState(
- getHistoryState(),
- '',
- getFullURL(this.createURL(qs))
- );
- },
- createURL(qs) {
- return this.character + qs + document.location.hash;
- },
- readUrl() {
- return window.location.search.slice(1);
- },
- dispose() {
- window.removeEventListener('popstate', this._onPopState);
- window.history.pushState(null, null, getFullURL(''));
- },
-};
-
-// we always push the full url to the url bar. Not a relative one.
-// So that we handle cases like using a , see
-// https://github.com/algolia/instantsearch.js/issues/790 for the original issue
-function getFullURL(relative) {
- return getLocationOrigin() + window.location.pathname + relative;
-}
-
-// IE <= 11 has no location.origin or buggy
-function getLocationOrigin() {
- // eslint-disable-next-line max-len
- return `${window.location.protocol}//${window.location.hostname}${
- window.location.port ? `:${window.location.port}` : ''
- }`;
-}
-
-// see InstantSearch.js file for urlSync options
-class URLSync {
- constructor(urlUtils, options) {
- this.urlUtils = urlUtils;
- this.originalConfig = null;
- this.mapping = options.mapping || {};
- this.getHistoryState = options.getHistoryState || (() => null);
- this.threshold = options.threshold || 700;
- this.trackedParameters = options.trackedParameters || [
- 'query',
- 'attribute:*',
- 'index',
- 'page',
- 'hitsPerPage',
- ];
- this.firstRender = true;
-
- this.searchParametersFromUrl = AlgoliaSearchHelper.getConfigurationFromQueryString(
- this.urlUtils.readUrl(),
- { mapping: this.mapping }
- );
- }
-
- init({ state }) {
- this.initState = state;
- }
-
- getConfiguration(currentConfiguration) {
- // we need to create a REAL helper to then get its state. Because some parameters
- // like hierarchicalFacet.rootPath are then triggering a default refinement that would
- // be not present if it was not going trough the SearchParameters constructor
- this.originalConfig = algoliasearchHelper(
- {},
- currentConfiguration.index,
- currentConfiguration
- ).state;
- return this.searchParametersFromUrl;
- }
-
- render({ helper, state }) {
- if (this.firstRender) {
- this.firstRender = false;
- this.onHistoryChange(this.onPopState.bind(this, helper));
- helper.on('change', s => this.renderURLFromState(s));
-
- const initStateQs = this.getQueryString(this.initState);
- const stateQs = this.getQueryString(state);
- if (initStateQs !== stateQs) {
- // force update the URL, if the state has changed since the initial URL read
- // We do this in order to make a URL update when there is search function
- // that prevent the search of the initial rendering
- // See: https://github.com/algolia/instantsearch.js/issues/2523#issuecomment-339356157
- this.renderURLFromState(state);
- }
- }
- }
-
- dispose({ helper }) {
- helper.removeListener('change', this.renderURLFromState);
- this.urlUtils.dispose();
- }
-
- onPopState(helper, fullState) {
- clearTimeout(this.urlUpdateTimeout);
- // compare with helper.state
- const partialHelperState = helper.getState(this.trackedParameters);
- const fullHelperState = {
- ...this.originalConfig,
- ...partialHelperState,
- };
-
- if (isEqual(fullHelperState, fullState)) return;
-
- helper.overrideStateWithoutTriggeringChangeEvent(fullState).search();
- }
-
- renderURLFromState(state) {
- const qs = this.getQueryString(state);
- clearTimeout(this.urlUpdateTimeout);
- this.urlUpdateTimeout = setTimeout(() => {
- this.urlUtils.pushState(qs, { getHistoryState: this.getHistoryState });
- }, this.threshold);
- }
-
- getQueryString(state) {
- const currentQueryString = this.urlUtils.readUrl();
- const foreignConfig = AlgoliaSearchHelper.getForeignConfigurationInQueryString(
- currentQueryString,
- { mapping: this.mapping }
- );
-
- return urlHelper.getQueryStringFromState(
- state.filter(this.trackedParameters),
- {
- moreAttributes: foreignConfig,
- mapping: this.mapping,
- safe: true,
- }
- );
- }
-
- // External APIs
-
- createURL(state, { absolute }) {
- const filteredState = state.filter(this.trackedParameters);
-
- const relative = this.urlUtils.createURL(
- algoliasearchHelper.url.getQueryStringFromState(filteredState, {
- mapping: this.mapping,
- })
- );
-
- return absolute ? getFullURL(relative) : relative;
- }
-
- onHistoryChange(fn) {
- this.urlUtils.onpopstate(() => {
- const qs = this.urlUtils.readUrl();
- const partialState = AlgoliaSearchHelper.getConfigurationFromQueryString(
- qs,
- { mapping: this.mapping }
- );
- const fullState = {
- ...this.originalConfig,
- ...partialState,
- };
- fn(fullState);
- });
- }
-}
-
-/**
- * Instantiate a url sync widget. This widget let you synchronize the search
- * parameters with the URL. It can operate with legacy API and hash or it can use
- * the modern history API. By default, it will use the modern API, but if you are
- * looking for compatibility with IE8 and IE9, then you should set 'useHash' to
- * true.
- * @param {object} options all the parameters to configure the URL synchronization. It
- * may contain the following keys :
- * - threshold:number time in ms after which a new state is created in the browser
- * history. The default value is 700.
- * - trackedParameters:string[] parameters that will be synchronized in the
- * URL. By default, it will track the query, all the refinable attributes (facets and numeric
- * filters), the index and the page.
- * - useHash:boolean if set to true, the url will be hash based. Otherwise,
- * it'll use the query parameters using the modern history API.
- * @return {object} the widget instance
- */
-function urlSync(options = {}) {
- const useHash = options.useHash || false;
- const customUrlUtils = options.urlUtils;
-
- const urlUtils = customUrlUtils || (useHash ? hashUrlUtils : modernUrlUtils);
-
- return new URLSync(urlUtils, options);
-}
-
-export default urlSync;
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 81bf79ac4e..a9ad009b9d 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -10,6 +10,7 @@ import curry from 'lodash/curry';
import hogan from 'hogan.js';
export {
+ capitalize,
getContainerNode,
bemHelper,
prepareTemplateProps,
@@ -17,7 +18,6 @@ export {
isSpecialClick,
isDomElement,
getRefinements,
- getAttributesToClear,
clearRefinements,
prefixKeys,
escapeRefinement,
@@ -26,9 +26,19 @@ export {
isReactElement,
deprecate,
warn,
- parseAroundLatLngFromString,
+ aroundLatLngToPosition,
+ insideBoundingBoxToBoundingBox,
};
+function capitalize(string) {
+ return (
+ string
+ .toString()
+ .charAt(0)
+ .toUpperCase() + string.toString().slice(1)
+ );
+}
+
/**
* Return the container. If it's a string, it is considered a
* css selector and retrieves the first matching element. Otherwise
@@ -105,18 +115,15 @@ function bemHelper(block) {
/**
* Prepares an object to be passed to the Template widget
* @param {object} unknownBecauseES6 an object with the following attributes:
- * - transformData
* - defaultTemplate
* - templates
* - templatesConfig
* @return {object} the configuration with the attributes:
- * - transformData
* - defaultTemplate
* - templates
* - useCustomCompileOptions
*/
function prepareTemplateProps({
- transformData,
defaultTemplates,
templates,
templatesConfig,
@@ -124,7 +131,6 @@ function prepareTemplateProps({
const preparedTemplates = prepareTemplates(defaultTemplates, templates);
return {
- transformData,
templatesConfig,
...preparedTemplates,
};
@@ -180,10 +186,16 @@ function renderTemplate({
data
);
- return hogan.compile(template, compileOptions).render({
- ...data,
- helpers: transformedHelpers,
- });
+ return hogan
+ .compile(template, compileOptions)
+ .render({
+ ...data,
+ helpers: transformedHelpers,
+ })
+ .replace(/[ \n\r\t\f\xA0]+/g, spaces =>
+ spaces.replace(/(^|\xA0+)[^\xA0]+/g, '$1 ')
+ )
+ .trim();
}
// We add all our template helper methods to the template as lambdas. Note
@@ -203,39 +215,35 @@ function getRefinement(state, type, attributeName, name, resultsFacets) {
const res = { type, attributeName, name };
let facet = find(resultsFacets, { name: attributeName });
let count;
+
if (type === 'hierarchical') {
const facetDeclaration = state.getHierarchicalFacetByName(attributeName);
const split = name.split(facetDeclaration.separator);
- res.name = split[split.length - 1];
+
for (let i = 0; facet !== undefined && i < split.length; ++i) {
facet = find(facet.data, { name: split[i] });
}
+
count = get(facet, 'count');
} else {
count = get(facet, `data["${res.name}"]`);
}
+
const exhaustive = get(facet, 'exhaustive');
+
if (count !== undefined) {
res.count = count;
}
+
if (exhaustive !== undefined) {
res.exhaustive = exhaustive;
}
+
return res;
}
function getRefinements(results, state, clearsQuery) {
- const res =
- clearsQuery && state.query && state.query.trim()
- ? [
- {
- type: 'query',
- name: state.query,
- query: state.query,
- attributeName: 'query',
- },
- ]
- : [];
+ const res = [];
forEach(state.facetsRefinements, (refinements, attributeName) => {
forEach(refinements, name => {
@@ -299,32 +307,28 @@ function getRefinements(results, state, clearsQuery) {
res.push({ type: 'tag', attributeName: '_tags', name });
});
+ if (clearsQuery && state.query && state.query.trim()) {
+ res.push({
+ attributeName: 'query',
+ type: 'query',
+ name: state.query,
+ query: state.query,
+ });
+ }
+
return res;
}
/**
* Clears the refinements of a SearchParameters object based on rules provided.
- * The white list is first used then the black list is applied. If no white list
- * is provided, all the current refinements are used.
+ * The included attributes list is applied before the excluded attributes list. If the list
+ * is not provided, this list of all the currently refined attributes is used as included attributes.
* @param {object} $0 parameters
* @param {Helper} $0.helper instance of the Helper
- * @param {string[]} [$0.whiteList] list of parameters to clear
- * @param {string[]} [$0.blackList=[]] list of parameters not to remove (will impact the white list)
- * @param {boolean} [$0.clearsQuery=false] clears the query if need be
+ * @param {string[]} [$0.attributesToClear = []] list of parameters to clear
* @returns {SearchParameters} search parameters with refinements cleared
*/
-function clearRefinements({
- helper,
- whiteList,
- blackList = [],
- clearsQuery = false,
-}) {
- const attributesToClear = getAttributesToClear({
- helper,
- whiteList,
- blackList,
- });
-
+function clearRefinements({ helper, attributesToClear = [] }) {
let finalState = helper.state;
attributesToClear.forEach(attribute => {
@@ -335,33 +339,13 @@ function clearRefinements({
}
});
- if (clearsQuery) {
+ if (attributesToClear.indexOf('query') !== -1) {
finalState = finalState.setQuery('');
}
return finalState;
}
-/**
- * Computes the list of attributes (conjunctive, disjunctive, hierarchical facet + numerical attributes)
- * to clear based on a optional white and black lists. The white list is applied first then the black list.
- * @param {object} $0 parameters
- * @param {Helper} $0.helper instance of the Helper
- * @param {string[]} [$0.whiteList] attributes to clear (defaults to all attributes)
- * @param {string[]} [$0.blackList=[]] attributes to keep, will override the white list
- * @returns {string[]} the list of attributes to clear based on the rules
- */
-function getAttributesToClear({ helper, whiteList, blackList }) {
- const lastResults = helper.lastResults || {};
- const attributesToClear =
- whiteList ||
- getRefinements(lastResults, helper.state).map(one => one.attributeName);
-
- return attributesToClear.filter(
- attribute => blackList.indexOf(attribute) === -1
- );
-}
-
function prefixKeys(prefix, obj) {
if (obj) {
return mapKeys(obj, (v, k) => prefix + k);
@@ -403,19 +387,19 @@ function isReactElement(object) {
);
}
-function logger(message) {
+function log(message) {
// eslint-disable-next-line no-console
console.warn(`[InstantSearch.js]: ${message.trim()}`);
}
function deprecate(fn, message) {
- let hasAlreadyPrint = false;
+ let hasAlreadyPrinted = false;
return function(...args) {
- if (!hasAlreadyPrint) {
- hasAlreadyPrint = true;
+ if (!hasAlreadyPrinted) {
+ hasAlreadyPrinted = true;
- logger(message);
+ log(message);
}
return fn(...args);
@@ -424,21 +408,21 @@ function deprecate(fn, message) {
warn.cache = {};
function warn(message) {
- const hasAlreadyPrint = warn.cache[message];
+ const hasAlreadyPrinted = warn.cache[message];
- if (!hasAlreadyPrint) {
+ if (!hasAlreadyPrinted) {
warn.cache[message] = true;
- logger(message);
+ log(message);
}
}
const latLngRegExp = /^(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)$/;
-function parseAroundLatLngFromString(value) {
+function aroundLatLngToPosition(value) {
const pattern = value.match(latLngRegExp);
- // Since the value provided is the one send with the query, the API should
- // throw an error due to the wrong format. So throw an error should be safe..
+ // Since the value provided is the one send with the request, the API should
+ // throw an error due to the wrong format. So throw an error should be safe.
if (!pattern) {
throw new Error(`Invalid value for "aroundLatLng" parameter: "${value}"`);
}
@@ -448,3 +432,63 @@ function parseAroundLatLngFromString(value) {
lng: parseFloat(pattern[2]),
};
}
+
+export function getPropertyByPath(object, path) {
+ const parts = path.split('.');
+
+ return parts.reduce((current, key) => current && current[key], object);
+}
+
+function insideBoundingBoxArrayToBoundingBox(value) {
+ const [[neLat, neLng, swLat, swLng] = []] = value;
+
+ // Since the value provided is the one send with the request, the API should
+ // throw an error due to the wrong format. So throw an error should be safe.
+ if (!neLat || !neLng || !swLat || !swLng) {
+ throw new Error(
+ `Invalid value for "insideBoundingBox" parameter: [${value}]`
+ );
+ }
+
+ return {
+ northEast: {
+ lat: neLat,
+ lng: neLng,
+ },
+ southWest: {
+ lat: swLat,
+ lng: swLng,
+ },
+ };
+}
+
+function insideBoundingBoxStringToBoundingBox(value) {
+ const [neLat, neLng, swLat, swLng] = value.split(',').map(parseFloat);
+
+ // Since the value provided is the one send with the request, the API should
+ // throw an error due to the wrong format. So throw an error should be safe.
+ if (!neLat || !neLng || !swLat || !swLng) {
+ throw new Error(
+ `Invalid value for "insideBoundingBox" parameter: "${value}"`
+ );
+ }
+
+ return {
+ northEast: {
+ lat: neLat,
+ lng: neLng,
+ },
+ southWest: {
+ lat: swLat,
+ lng: swLng,
+ },
+ };
+}
+
+function insideBoundingBoxToBoundingBox(value) {
+ if (Array.isArray(value)) {
+ return insideBoundingBoxArrayToBoundingBox(value);
+ }
+
+ return insideBoundingBoxStringToBoundingBox(value);
+}
diff --git a/src/lib/version.js b/src/lib/version.js
index 5703259c7f..535701a535 100644
--- a/src/lib/version.js
+++ b/src/lib/version.js
@@ -1 +1 @@
-export default '2.10.4';
+export default '3.0.0-beta.2';
diff --git a/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap b/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap
index cf3d1f4857..8ff27b0c2f 100644
--- a/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap
+++ b/src/widgets/breadcrumb/__tests__/__snapshots__/breadcrumb-test.js.snap
@@ -1,44 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`breadcrumb() render renders transformed items correctly 1`] = `
- Digital Cameras",
},
Object {
- "name": "Digital Cameras",
+ "label": "Digital Cameras",
"transformed": true,
"value": null,
},
]
}
refine={[Function]}
- separator=" > "
- shouldAutoHideContainer={false}
templateProps={
Object {
"templates": Object {
"home": "Home",
- "separator": "",
+ "separator": ">",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
"home": false,
"separator": false,
diff --git a/src/widgets/breadcrumb/breadcrumb.js b/src/widgets/breadcrumb/breadcrumb.js
index fd61b449f5..7f04d0d2be 100644
--- a/src/widgets/breadcrumb/breadcrumb.js
+++ b/src/widgets/breadcrumb/breadcrumb.js
@@ -1,27 +1,14 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
import Breadcrumb from '../../components/Breadcrumb/Breadcrumb';
import connectBreadcrumb from '../../connectors/breadcrumb/connectBreadcrumb';
import defaultTemplates from './defaultTemplates.js';
+import { getContainerNode, prepareTemplateProps } from '../../lib/utils';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- getContainerNode,
- prepareTemplateProps,
-} from '../../lib/utils';
-
-const bem = bemHelper('ais-breadcrumb');
+const suit = component('Breadcrumb');
-const renderer = ({
- autoHideContainer,
- containerNode,
- cssClasses,
- renderState,
- separator,
- templates,
- transformData,
-}) => (
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
{ canRefine, createURL, instantSearchInstance, items, refine },
isFirstRendering
) => {
@@ -30,13 +17,11 @@ const renderer = ({
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
- transformData,
});
+
return;
}
- const shouldAutoHideContainer = autoHideContainer && !canRefine;
-
render(
,
containerNode
@@ -56,44 +39,39 @@ const usage = `Usage:
breadcrumb({
container,
attributes,
- [ autoHideContainer=true ],
- [ cssClasses.{disabledLabel, home, label, root, separator}={} ],
- [ templates.{home, separator}]
- [ transformData.{item} ],
+ [ separator = ' > ' ],
+ [ rootPath = null ],
[ transformItems ],
+ [ templates.{home, separator}],
+ [ cssClasses.{root, noRefinement, list, item, selectedItem, separator, link} ],
})`;
/**
* @typedef {Object} BreadcrumbCSSClasses
- * @property {string|string[]} [disabledLabel] CSS class to add to the last element of the breadcrumb (which is not clickable).
- * @property {string|string[]} [home] CSS class to add to the first element of the breadcrumb.
- * @property {string|string[]} [label] CSS class to add to the text part of each element of the breadcrumb.
* @property {string|string[]} [root] CSS class to add to the root element of the widget.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element of the widget if there are no refinements.
+ * @property {string|string[]} [list] CSS class to add to the list element.
+ * @property {string|string[]} [item] CSS class to add to the items of the list. The items contains the link and the separator.
+ * @property {string|string[]} [selectedItem] CSS class to add to the selected item in the list: the last one or the home if there are no refinements.
* @property {string|string[]} [separator] CSS class to add to the separator.
+ * @property {string|string[]} [link] CSS class to add to the links in the items.
*/
/**
* @typedef {Object} BreadcrumbTemplates
- * @property {string|function(object):string} [home='Home'] Label of the breadcrumb's first element.
- * @property {string|function(object):string} [separator=''] Symbol used to separate the elements of the breadcrumb.
- */
-
-/**
- * @typedef {Object} BreadcrumbTransforms
- * @property {function(object):object} [item] Method to change the object passed to the `item` template
+ * @property {string|function(object):string} [home = 'Home'] Label of the breadcrumb's first element.
+ * @property {string|function(object):string} [separator = '>'] Symbol used to separate the elements of the breadcrumb.
*/
/**
* @typedef {Object} BreadcrumbWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
* @property {string[]} attributes Array of attributes to use to generate the breadcrumb.
- *
- * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
+ * @property {string} [separator = ' > '] The level separator used in the records.
+ * @property {string} [rootPath = null] Prefix path to use if the first level is not the root level.
* @property {BreadcrumbTemplates} [templates] Templates to use for the widget.
- * @property {BreadcrumbTransforms} [transformData] Set of functions to transform the data passed to the templates.
- * @property {boolean} [autoHideContainer=true] Hides the container when there are no items in the breadcrumb.
- * @property {BreadcrumbCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ * @property {BreadcrumbCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
*/
/**
@@ -144,21 +122,20 @@ breadcrumb({
* container: '#breadcrumb',
* attributes: ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1', 'hierarchicalCategories.lvl2'],
* templates: { home: 'Home Page' },
+ * separator: ' / ',
* rootPath: 'Cameras & Camcorders > Digital Cameras',
* })
* );
*/
export default function breadcrumb({
- attributes,
- autoHideContainer = false,
container,
- cssClasses: userCssClasses = {},
+ attributes,
+ separator,
rootPath = null,
- separator = ' > ',
- templates = defaultTemplates,
- transformData,
transformItems,
+ templates = defaultTemplates,
+ cssClasses: userCssClasses = {},
} = {}) {
if (!container) {
throw new Error(usage);
@@ -167,30 +144,37 @@ export default function breadcrumb({
const containerNode = getContainerNode(container);
const cssClasses = {
- disabledLabel: cx(bem('disabledLabel'), userCssClasses.disabledLabel),
- home: cx(bem('home'), userCssClasses.home),
- item: cx(bem('item'), userCssClasses.item),
- label: cx(bem('label'), userCssClasses.label),
- root: cx(bem('root'), userCssClasses.root),
- separator: cx(bem('separator'), userCssClasses.separator),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ separator: cx(
+ suit({ descendantName: 'separator' }),
+ userCssClasses.separator
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
};
const specializedRenderer = renderer({
- autoHideContainer,
containerNode,
cssClasses,
renderState: {},
- separator,
templates,
- transformData,
});
try {
const makeBreadcrumb = connectBreadcrumb(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeBreadcrumb({ attributes, rootPath, transformItems });
- } catch (e) {
+ return makeBreadcrumb({ attributes, separator, rootPath, transformItems });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/breadcrumb/defaultTemplates.js b/src/widgets/breadcrumb/defaultTemplates.js
index 270fbf4d14..ca7e11f986 100644
--- a/src/widgets/breadcrumb/defaultTemplates.js
+++ b/src/widgets/breadcrumb/defaultTemplates.js
@@ -1,4 +1,4 @@
export default {
home: 'Home',
- separator: '',
+ separator: '>',
};
diff --git a/src/widgets/clear-all/__tests__/__snapshots__/clear-all-test.js.snap b/src/widgets/clear-all/__tests__/__snapshots__/clear-all-test.js.snap
deleted file mode 100644
index d18fc007bd..0000000000
--- a/src/widgets/clear-all/__tests__/__snapshots__/clear-all-test.js.snap
+++ /dev/null
@@ -1,141 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`clearAll() with refinements calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`clearAll() with refinements calls twice ReactDOM.render( , container) 2`] = `
-
-`;
-
-exports[`clearAll() without refinements calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`clearAll() without refinements calls twice ReactDOM.render( , container) 2`] = `
-
-`;
diff --git a/src/widgets/clear-all/__tests__/clear-all-test.js b/src/widgets/clear-all/__tests__/clear-all-test.js
deleted file mode 100644
index 0562ab3e4b..0000000000
--- a/src/widgets/clear-all/__tests__/clear-all-test.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import expect from 'expect';
-import sinon from 'sinon';
-import clearAll from '../clear-all';
-import defaultTemplates from '../defaultTemplates.js';
-
-describe('clearAll()', () => {
- let ReactDOM;
- let container;
- let widget;
- let props;
- let results;
- let helper;
- let createURL;
-
- beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
- createURL = sinon.stub().returns('#all-cleared');
-
- clearAll.__Rewire__('render', ReactDOM.render);
-
- container = document.createElement('div');
- widget = clearAll({
- container,
- autoHideContainer: true,
- cssClasses: { root: ['root', 'cx'] },
- });
-
- results = {};
- helper = {
- state: {
- clearRefinements: sinon.stub().returnsThis(),
- clearTags: sinon.stub().returnsThis(),
- },
- search: sinon.spy(),
- };
-
- props = {
- refine: sinon.spy(),
- cssClasses: {
- root: 'ais-clear-all root cx',
- header: 'ais-clear-all--header',
- body: 'ais-clear-all--body',
- footer: 'ais-clear-all--footer',
- link: 'ais-clear-all--link',
- },
- collapsible: false,
- hasRefinements: false,
- shouldAutoHideContainer: true,
- templateProps: {
- templates: defaultTemplates,
- templatesConfig: {},
- transformData: undefined,
- useCustomCompileOptions: { header: false, footer: false, link: false },
- },
- url: '#all-cleared',
- };
- widget.init({
- helper,
- createURL,
- instantSearchInstance: {
- templatesConfig: {},
- },
- });
- });
-
- it('configures nothing', () => {
- expect(widget.getConfiguration).toEqual(undefined);
- });
-
- describe('without refinements', () => {
- beforeEach(() => {
- helper.state.facetsRefinements = {};
- props.hasRefinements = false;
- props.shouldAutoHideContainer = true;
- });
-
- it('calls twice ReactDOM.render( , container)', () => {
- widget.render({
- results,
- helper,
- state: helper.state,
- createURL,
- instantSearchInstance: {},
- });
- widget.render({
- results,
- helper,
- state: helper.state,
- createURL,
- instantSearchInstance: {},
- });
-
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
- });
- });
-
- describe('with refinements', () => {
- beforeEach(() => {
- helper.state.facetsRefinements = ['something'];
- props.hasRefinements = true;
- props.shouldAutoHideContainer = false;
- });
-
- it('calls twice ReactDOM.render( , container)', () => {
- widget.render({ results, helper, state: helper.state, createURL });
- widget.render({ results, helper, state: helper.state, createURL });
-
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
- });
- });
-
- afterEach(() => {
- clearAll.__ResetDependency__('render');
- clearAll.__ResetDependency__('defaultTemplates');
- });
-});
diff --git a/src/widgets/clear-all/clear-all.js b/src/widgets/clear-all/clear-all.js
deleted file mode 100644
index 5ecc740550..0000000000
--- a/src/widgets/clear-all/clear-all.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import ClearAllWithHOCs from '../../components/ClearAll/ClearAll.js';
-import cx from 'classnames';
-
-import {
- bemHelper,
- getContainerNode,
- prepareTemplateProps,
-} from '../../lib/utils.js';
-
-import connectClearAll from '../../connectors/clear-all/connectClearAll.js';
-
-import defaultTemplates from './defaultTemplates.js';
-
-const bem = bemHelper('ais-clear-all');
-
-const renderer = ({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState,
- templates,
-}) => (
- { refine, hasRefinements, createURL, instantSearchInstance },
- isFirstRendering
-) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- const shouldAutoHideContainer = autoHideContainer && !hasRefinements;
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-clearAll({
- container,
- [ cssClasses.{root,header,body,footer,link}={} ],
- [ templates.{header,link,footer}={link: 'Clear all'} ],
- [ autoHideContainer=true ],
- [ collapsible=false ],
- [ excludeAttributes=[] ]
-})`;
-/**
- * @typedef {Object} ClearAllCSSClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- * @property {string|string[]} [link] CSS class to add to the link element.
- */
-
-/**
- * @typedef {Object} ClearAllTemplates
- * @property {string|function(object):string} [header] Header template.
- * @property {string|function(object):string} [link] Link template.
- * @property {string|function(object):string} [footer] Footer template.
- */
-
-/**
- * @typedef {Object} ClearAllWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {string[]} [excludeAttributes] List of attributes names to exclude from clear actions.
- * @property {ClearAllTemplates} [templates] Templates to use for the widget.
- * @property {boolean} [autoHideContainer=true] Hide the container when there are no refinements to clear.
- * @property {ClearAllCSSClasses} [cssClasses] CSS classes to be added.
- * @property {boolean|{collapsed: boolean}} [collapsible=false] Makes the widget collapsible. The user can then.
- * choose to hide the content of the widget. This option can also be an object with the property collapsed. If this
- * property is `true`, then the widget is hidden during the first rendering.
- * @property {boolean} [clearsQuery = false] If true, the widget will also clear the query.
- */
-
-/**
- * The clear all widget gives the user the ability to clear all the refinements currently
- * applied on the results. It is equivalent to the reset button of a form.
- *
- * The current refined values widget can display a button that has the same behavior.
- * @type {WidgetFactory}
- * @devNovel ClearAll
- * @category clear-filter
- * @param {ClearAllWidgetOptions} $0 The ClearAll widget options.
- * @returns {Widget} A new instance of the ClearAll widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.clearAll({
- * container: '#clear-all',
- * templates: {
- * link: 'Reset everything'
- * },
- * autoHideContainer: false,
- * clearsQuery: true,
- * })
- * );
- */
-export default function clearAll({
- container,
- templates = defaultTemplates,
- cssClasses: userCssClasses = {},
- collapsible = false,
- autoHideContainer = true,
- excludeAttributes = [],
- clearsQuery = false,
-}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- link: cx(bem('link'), userCssClasses.link),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState: {},
- templates,
- });
-
- try {
- const makeWidget = connectClearAll(specializedRenderer, () =>
- unmountComponentAtNode(containerNode)
- );
- return makeWidget({ excludeAttributes, clearsQuery });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/clear-all/defaultTemplates.js b/src/widgets/clear-all/defaultTemplates.js
deleted file mode 100644
index ee1bcf132a..0000000000
--- a/src/widgets/clear-all/defaultTemplates.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
- header: '',
- link: 'Clear all',
- footer: '',
-};
diff --git a/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap b/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap
new file mode 100644
index 0000000000..4bfab60e25
--- /dev/null
+++ b/src/widgets/clear-refinements/__tests__/__snapshots__/clear-refinements-test.js.snap
@@ -0,0 +1,117 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`clearRefinements() cssClasses should add the default CSS classes 1`] = `
+Object {
+ "button": "ais-ClearRefinements-button",
+ "disabledButton": "ais-ClearRefinements-button--disabled",
+ "root": "ais-ClearRefinements",
+}
+`;
+
+exports[`clearRefinements() cssClasses should allow overriding CSS classes 1`] = `
+Object {
+ "button": "ais-ClearRefinements-button myButton myPrimaryButton",
+ "disabledButton": "ais-ClearRefinements-button--disabled disabled",
+ "root": "ais-ClearRefinements myRoot",
+}
+`;
+
+exports[`clearRefinements() with refinements calls twice ReactDOM.render( , container) 1`] = `
+
+`;
+
+exports[`clearRefinements() with refinements calls twice ReactDOM.render( , container) 2`] = `
+
+`;
+
+exports[`clearRefinements() without refinements calls twice ReactDOM.render( , container) 1`] = `
+
+`;
+
+exports[`clearRefinements() without refinements calls twice ReactDOM.render( , container) 2`] = `
+
+`;
diff --git a/src/widgets/clear-refinements/__tests__/clear-refinements-test.js b/src/widgets/clear-refinements/__tests__/clear-refinements-test.js
new file mode 100644
index 0000000000..be575dc92a
--- /dev/null
+++ b/src/widgets/clear-refinements/__tests__/clear-refinements-test.js
@@ -0,0 +1,150 @@
+import clearRefinements from '../clear-refinements';
+import algoliasearchHelper from 'algoliasearch-helper';
+import algoliasearch from 'algoliasearch';
+
+describe('clearRefinements()', () => {
+ let ReactDOM;
+ let container;
+ let results;
+ let client;
+ let helper;
+ let createURL;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ createURL = jest.fn().mockReturnValue('#all-cleared');
+
+ clearRefinements.__Rewire__('render', ReactDOM.render);
+
+ container = document.createElement('div');
+ results = {};
+ client = algoliasearch('APP_ID', 'API_KEY');
+ helper = {
+ state: {
+ clearRefinements: jest.fn().mockReturnThis(),
+ clearTags: jest.fn().mockReturnThis(),
+ },
+ search: jest.fn(),
+ };
+ });
+
+ describe('without refinements', () => {
+ beforeEach(() => {
+ helper.state.facetsRefinements = {};
+ });
+
+ it('calls twice ReactDOM.render( , container)', () => {
+ const widget = clearRefinements({
+ container,
+ });
+
+ widget.init({
+ helper,
+ createURL,
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+ widget.render({
+ results,
+ helper,
+ state: helper.state,
+ createURL,
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results,
+ helper,
+ state: helper.state,
+ createURL,
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+ });
+
+ describe('with refinements', () => {
+ beforeEach(() => {
+ helper.state.facetsRefinements = ['something'];
+ });
+
+ it('calls twice ReactDOM.render( , container)', () => {
+ const widget = clearRefinements({
+ container,
+ });
+ widget.init({
+ helper,
+ createURL,
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+ widget.render({ results, helper, state: helper.state, createURL });
+ widget.render({ results, helper, state: helper.state, createURL });
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+ });
+
+ describe('cssClasses', () => {
+ it('should add the default CSS classes', () => {
+ helper = algoliasearchHelper(client, 'index_name');
+ const widget = clearRefinements({
+ container,
+ });
+
+ widget.init({
+ helper,
+ createURL,
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+
+ widget.render({ results, helper, state: helper.state, createURL });
+ expect(
+ ReactDOM.render.mock.calls[0][0].props.cssClasses
+ ).toMatchSnapshot();
+ });
+
+ it('should allow overriding CSS classes', () => {
+ const widget = clearRefinements({
+ container,
+ cssClasses: {
+ root: 'myRoot',
+ button: ['myButton', 'myPrimaryButton'],
+ disabledButton: ['disabled'],
+ },
+ });
+ widget.init({
+ helper,
+ createURL,
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+ widget.render({ results, helper, state: helper.state, createURL });
+
+ expect(
+ ReactDOM.render.mock.calls[0][0].props.cssClasses
+ ).toMatchSnapshot();
+ });
+ });
+
+ afterEach(() => {
+ clearRefinements.__ResetDependency__('render');
+ });
+});
diff --git a/src/widgets/clear-refinements/clear-refinements.js b/src/widgets/clear-refinements/clear-refinements.js
new file mode 100644
index 0000000000..40d4b5da62
--- /dev/null
+++ b/src/widgets/clear-refinements/clear-refinements.js
@@ -0,0 +1,128 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import ClearRefinements from '../../components/ClearRefinements/ClearRefinements.js';
+import cx from 'classnames';
+import connectClearRefinements from '../../connectors/clear-refinements/connectClearRefinements.js';
+import defaultTemplates from './defaultTemplates.js';
+import { getContainerNode, prepareTemplateProps } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
+
+const suit = component('ClearRefinements');
+
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
+ { refine, hasRefinements, instantSearchInstance },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ renderState.templateProps = prepareTemplateProps({
+ defaultTemplates,
+ templatesConfig: instantSearchInstance.templatesConfig,
+ templates,
+ });
+ return;
+ }
+
+ render(
+ ,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+clearRefinements({
+ container,
+ [ includedAttributes = [] ],
+ [ excludedAttributes = ['query'] ],
+ [ transformItems ],
+ [ templates.{resetLabel} ],
+ [ cssClasses.{root, button, disabledButton} ],
+})`;
+/**
+ * @typedef {Object} ClearRefinementsCSSClasses
+ * @property {string|string[]} [root] CSS class to add to the wrapper element.
+ * @property {string|string[]} [button] CSS class to add to the button of the widget.
+ * @property {string|string[]} [disabledButton] CSS class to add to the button when there are no refinements.
+ */
+
+/**
+ * @typedef {Object} ClearRefinementsTemplates
+ * @property {string|string[]} [resetLabel] Template for the content of the button
+ */
+
+/**
+ * @typedef {Object} ClearRefinementsWidgetOptions
+ * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
+ * @property {string[]} [includedAttributes = []] The attributes to include in the refinements to clear (all by default). Cannot be used with `excludedAttributes`.
+ * @property {string[]} [excludedAttributes = ['query']] The attributes to exclude from the refinements to clear. Cannot be used with `includedAttributes`.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ * @property {ClearRefinementsTemplates} [templates] Templates to use for the widget.
+ * @property {ClearRefinementsCSSClasses} [cssClasses] CSS classes to be added.
+ */
+
+/**
+ * The clear all widget gives the user the ability to clear all the refinements currently
+ * applied on the results. It is equivalent to the reset button of a form.
+ *
+ * The current refined values widget can display a button that has the same behavior.
+ * @type {WidgetFactory}
+ * @devNovel ClearRefinements
+ * @category clear-filter
+ * @param {ClearRefinementsWidgetOptions} $0 The ClearRefinements widget options.
+ * @returns {Widget} A new instance of the ClearRefinements widget.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.clearRefinements({
+ * container: '#clear-all',
+ * templates: {
+ * resetLabel: 'Reset everything'
+ * },
+ * })
+ * );
+ */
+export default function clearRefinements({
+ container,
+ templates = defaultTemplates,
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ cssClasses: userCssClasses = {},
+}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ button: cx(suit({ descendantName: 'button' }), userCssClasses.button),
+ disabledButton: cx(
+ suit({ descendantName: 'button', modifierName: 'disabled' }),
+ userCssClasses.disabledButton
+ ),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ renderState: {},
+ templates,
+ });
+
+ try {
+ const makeWidget = connectClearRefinements(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeWidget({
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/clear-refinements/defaultTemplates.js b/src/widgets/clear-refinements/defaultTemplates.js
new file mode 100644
index 0000000000..0191ef7dc5
--- /dev/null
+++ b/src/widgets/clear-refinements/defaultTemplates.js
@@ -0,0 +1,3 @@
+export default {
+ resetLabel: 'Clear refinements',
+};
diff --git a/src/widgets/configure/configure.js b/src/widgets/configure/configure.js
index e704a6b8cd..e49099bc02 100644
--- a/src/widgets/configure/configure.js
+++ b/src/widgets/configure/configure.js
@@ -3,7 +3,7 @@ import connectConfigure from '../../connectors/configure/connectConfigure.js';
const usage = `Usage:
search.addWidget(
instantsearch.widgets.configure({
- // any searchParameter
+ // any search parameter: https://www.algolia.com/doc/api-reference/search-api-parameters/
})
);
Full documentation available at https://community.algolia.com/instantsearch.js/v2/widgets/configure.html
@@ -35,7 +35,7 @@ export default function configure(searchParameters) {
// We do not have default renderFn && unmountFn for this widget
const makeWidget = connectConfigure();
return makeWidget({ searchParameters });
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap b/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap
deleted file mode 100644
index 409fdc1bcc..0000000000
--- a/src/widgets/current-refined-values/__tests__/__snapshots__/current-refined-values-test.js.snap
+++ /dev/null
@@ -1,3895 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === false should render all attributes with an empty array 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === false should render all attributes with not defined attributes 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === false should render and pass all attributes defined in each objects 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === true should render all attributes with an empty array 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === true should render all attributes with not defined attributes 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes === true should render and pass all attributes defined in each objects 1`] = `
-
-`;
-
-exports[`currentRefinedValues() render() options.attributes with options.onlyListedAttributes not defined should default to false 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.autoHideContainer with refinements shouldAutoHideContainer should be false with autoHideContainer = false 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.autoHideContainer with refinements shouldAutoHideContainer should be false with autoHideContainer = true 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.autoHideContainer without refinements shouldAutoHideContainer should be false with autoHideContainer = false 1`] = `
-
-`;
-
-exports[`currentRefinedValues() render() options.autoHideContainer without refinements shouldAutoHideContainer should be true with autoHideContainer = true 1`] = `
-
-`;
-
-exports[`currentRefinedValues() render() options.clearAll should pass it as clearAllPosition 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.container should render with a HTMLElement container 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.container should render with a string container 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.cssClasses should be passed in the cssClasses 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.cssClasses should work with an array 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.templates should pass it in templateProps 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "MY CUSTOM TEMPLATE",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() options.transformItems should transform passed items 1`] = `
-=",
- "transformed": true,
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "transformed": true,
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "transformed": true,
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "transformed": true,
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "transformed": true,
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "transformed": true,
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() should render twice 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() should render twice 2`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
-
-exports[`currentRefinedValues() render() with attributes should sort the refinements according to their order 1`] = `
-=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericFacet",
- "computedLabel": "≤ 2",
- "name": "2",
- "numericValue": 2,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≥ 3",
- "name": "3",
- "numericValue": 3,
- "operator": ">=",
- "type": "numeric",
- },
- Object {
- "attributeName": "numericDisjunctiveFacet",
- "computedLabel": "≤ 4",
- "name": "4",
- "numericValue": 4,
- "operator": "<=",
- "type": "numeric",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag1",
- "name": "tag1",
- "type": "tag",
- },
- Object {
- "attributeName": "_tags",
- "computedLabel": "tag2",
- "name": "tag2",
- "type": "tag",
- },
- ]
- }
- shouldAutoHideContainer={false}
- templateProps={
- Object {
- "templates": Object {
- "clearAll": "CLEAR ALL",
- "footer": "FOOTER",
- "header": "HEADER",
- "item": "ITEM",
- },
- "templatesConfig": Object {
- "randomAttributeNeverUsed": "value",
- },
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "clearAll": true,
- "footer": true,
- "header": true,
- "item": true,
- },
- }
- }
-/>
-`;
diff --git a/src/widgets/current-refined-values/__tests__/__snapshots__/defaultTemplates-test.js.snap b/src/widgets/current-refined-values/__tests__/__snapshots__/defaultTemplates-test.js.snap
deleted file mode 100644
index 78352420fc..0000000000
--- a/src/widgets/current-refined-values/__tests__/__snapshots__/defaultTemplates-test.js.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`current-refined-values defaultTemplates \`item\` template does not show \`count\` when query refinement 1`] = `"Query : Samsu "`;
-
-exports[`current-refined-values defaultTemplates \`item\` template has a \`item\` default template 1`] = `"Brand : Samsung 4 "`;
-
-exports[`current-refined-values defaultTemplates \`item\` template wraps query refinements with 1`] = `"Query : Samsu "`;
diff --git a/src/widgets/current-refined-values/__tests__/current-refined-values-test.js b/src/widgets/current-refined-values/__tests__/current-refined-values-test.js
deleted file mode 100644
index 7f4807c5c1..0000000000
--- a/src/widgets/current-refined-values/__tests__/current-refined-values-test.js
+++ /dev/null
@@ -1,1155 +0,0 @@
-import algoliasearch from 'algoliasearch';
-import algoliasearchHelper from 'algoliasearch-helper';
-import { prepareTemplateProps } from '../../../lib/utils';
-import currentRefinedValues from '../current-refined-values';
-import defaultTemplates from '../defaultTemplates';
-
-describe('currentRefinedValues()', () => {
- describe('types checking', () => {
- let boundWidget;
- let parameters;
-
- beforeEach(() => {
- parameters = {
- container: document.createElement('div'),
- attributes: [],
- onlyListedAttributes: false,
- clearAll: 'after',
- templates: {},
- transformData: {
- item: data => data,
- },
- autoHideContainer: false,
- cssClasses: {},
- };
- boundWidget = currentRefinedValues.bind(null, parameters);
- });
-
- describe('options.container', () => {
- it("doesn't throw usage with a string", () => {
- const element = document.createElement('div');
- element.id = 'testid2';
- document.body.appendChild(element);
- parameters.container = '#testid2';
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with a HTMLElement", () => {
- parameters.container = document.createElement('div');
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage if not defined', () => {
- delete parameters.container;
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage with another type than string or HTMLElement', () => {
- parameters.container = true;
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.attributes', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.attributes;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if attributes is an empty array", () => {
- parameters.attributes = [];
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with name, label, template and transformData", () => {
- parameters.attributes = [
- {
- name: 'attr1',
- },
- {
- name: 'attr2',
- label: 'Attr 2',
- },
- {
- name: 'attr3',
- template: 'SPECIFIC TEMPLATE',
- },
- {
- name: 'attr4',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- ];
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with a function template", () => {
- parameters.attributes = [
- { name: 'attr1' },
- { name: 'attr2', template: () => 'CUSTOM TEMPLATE' },
- ];
- expect(boundWidget).not.toThrow();
- });
-
- it("throws usage if attributes isn't an array", () => {
- parameters.attributes = 'a string';
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it("throws usage if attributes doesn't contain only objects", () => {
- parameters.attributes = [{ name: 'test' }, 'string'];
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if attributes contains an object without name', () => {
- parameters.attributes = [{ name: 'test' }, { label: '' }];
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if attributes contains an object with a not string name', () => {
- parameters.attributes = [{ name: 'test' }, { name: true }];
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if attributes contains an object with a not string label', () => {
- parameters.attributes = [{ name: 'test' }, { label: true }];
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if attributes contains an object with a not string or function template', () => {
- parameters.attributes = [{ name: 'test' }, { template: true }];
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if attributes contains an object with a not function transformData', () => {
- parameters.attributes = [{ name: 'test' }, { transformData: true }];
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.onlyListedAttributes', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.onlyListedAttributes;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if true", () => {
- parameters.onlyListedAttributes = true;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if false", () => {
- parameters.onlyListedAttributes = false;
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage if not boolean', () => {
- parameters.onlyListedAttributes = 'truthy value';
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.clearAll', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.clearAll;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if false", () => {
- parameters.clearAll = false;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if 'before'", () => {
- parameters.clearAll = 'before';
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage if 'after'", () => {
- parameters.clearAll = 'after';
- expect(boundWidget).not.toThrow();
- });
-
- it("throws usage if not one of [false, 'before', 'after']", () => {
- parameters.clearAll = 'truthy value';
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.templates', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.templates;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with an empty object", () => {
- parameters.templates = {};
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with a string template", () => {
- parameters.templates = {
- item: 'STRING TEMPLATE',
- };
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with a function template", () => {
- parameters.templates = {
- item: () => 'ITEM TEMPLATE',
- };
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with all keys", () => {
- parameters.templates = {
- header: 'HEADER TEMPLATE',
- item: 'ITEM TEMPLATE',
- clearAll: 'CLEAR ALL TEMPLATE',
- footer: 'FOOTER TEMPLATE',
- };
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage with template being something else than an object', () => {
- parameters.templates = true;
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it("throws usage if one of the template keys doesn't exist", () => {
- parameters.templates = {
- header: 'HEADER TEMPLATE',
- notExistingKey: 'NOT EXISTING KEY TEMPLATE',
- };
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if a template is not a string or a function', () => {
- parameters.templates = {
- item: true,
- };
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.transformData', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.transformData;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with a function", () => {
- parameters.transformData = data => data;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with an object of functions", () => {
- parameters.transformData = {
- item: data => data,
- };
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage if not a function', () => {
- parameters.transformData = true;
- expect(boundWidget).toThrow();
- });
- });
-
- describe('options.autoHideContainer', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.autoHideContainer;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with true", () => {
- parameters.autoHideContainer = true;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with false", () => {
- parameters.autoHideContainer = false;
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage if not boolean', () => {
- parameters.autoHideContainer = 'truthy value';
- expect(boundWidget).toThrow(/Usage/);
- });
- });
-
- describe('options.cssClasses', () => {
- it("doesn't throw usage if not defined", () => {
- delete parameters.cssClasses;
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with an empty object", () => {
- parameters.cssClasses = {};
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with string class", () => {
- parameters.cssClasses = {
- item: 'item-class',
- };
- expect(boundWidget).not.toThrow();
- });
-
- it("doesn't throw usage with all keys", () => {
- parameters.cssClasses = {
- root: 'root-class',
- header: 'header-class',
- body: 'body-class',
- clearAll: 'clear-all-class',
- list: 'list-class',
- item: 'item-class',
- link: 'link-class',
- count: 'count-class',
- footer: 'footer-class',
- };
- expect(boundWidget).not.toThrow();
- });
-
- it('throws usage with cssClasses being something else than an object', () => {
- parameters.cssClasses = 'truthy value';
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it("throws usage if one of the cssClasses keys doesn't exist", () => {
- parameters.cssClasses = {
- notExistingKey: 'not-existing-class',
- };
- expect(boundWidget).toThrow(/Usage/);
- });
-
- it('throws usage if one of the cssClasses values is not a string', () => {
- parameters.cssClasses = {
- item: true,
- };
- expect(boundWidget).toThrow(/Usage/);
- });
- });
- });
-
- describe('getConfiguration()', () => {
- it('configures nothing', () => {
- const widget = currentRefinedValues({
- container: document.createElement('div'),
- });
- expect(widget.getConfiguration).toEqual(undefined);
- });
- });
-
- describe('render()', () => {
- let ReactDOM;
-
- let parameters;
- let client;
- let helper;
- let initParameters;
- let renderParameters;
- let refinements;
- let expectedProps;
-
- function setRefinementsInExpectedProps() {
- expectedProps.refinements = refinements;
- expectedProps.clearRefinementClicks = refinements.map(() => () => {});
- expectedProps.clearRefinementURLs = refinements.map(() => '#cleared');
- }
-
- beforeEach(() => {
- ReactDOM = { render: jest.fn() };
- currentRefinedValues.__Rewire__('render', ReactDOM.render);
-
- parameters = {
- container: document.createElement('div'),
- attributes: [
- { name: 'facet' },
- { name: 'facetExclude' },
- { name: 'disjunctiveFacet' },
- { name: 'hierarchicalFacet' },
- { name: 'numericFacet' },
- { name: 'numericDisjunctiveFacet' },
- { name: '_tags' },
- ],
- onlyListedAttributes: true,
- clearAll: 'after',
- templates: {
- header: 'HEADER',
- item: 'ITEM',
- clearAll: 'CLEAR ALL',
- footer: 'FOOTER',
- },
- autoHideContainer: false,
- cssClasses: {
- root: 'root-css-class',
- header: 'header-css-class',
- body: 'body-css-class',
- clearAll: 'clear-all-css-class',
- list: 'list-css-class',
- item: 'item-css-class',
- link: 'link-css-class',
- count: 'count-css-class',
- footer: 'footer-css-class',
- },
- };
-
- client = algoliasearch('APP_ID', 'API_KEY');
- helper = algoliasearchHelper(client, 'index_name', {
- facets: ['facet', 'facetExclude', 'numericFacet', 'extraFacet'],
- disjunctiveFacets: ['disjunctiveFacet', 'numericDisjunctiveFacet'],
- hierarchicalFacets: [
- {
- name: 'hierarchicalFacet',
- attributes: ['hierarchicalFacet.lvl0', 'hierarchicalFacet.lvl1'],
- separator: ' > ',
- },
- ],
- });
- helper
- .toggleRefinement('facet', 'facet-val1')
- .toggleRefinement('facet', 'facet-val2')
- .toggleRefinement('extraFacet', 'extraFacet-val1')
- .toggleFacetExclusion('facetExclude', 'facetExclude-val1')
- .toggleFacetExclusion('facetExclude', 'facetExclude-val2')
- .toggleRefinement('disjunctiveFacet', 'disjunctiveFacet-val1')
- .toggleRefinement('disjunctiveFacet', 'disjunctiveFacet-val2')
- .toggleRefinement(
- 'hierarchicalFacet',
- 'hierarchicalFacet-val1 > hierarchicalFacet-val2'
- )
- .addNumericRefinement('numericFacet', '>=', 1)
- .addNumericRefinement('numericFacet', '<=', 2)
- .addNumericRefinement('numericDisjunctiveFacet', '>=', 3)
- .addNumericRefinement('numericDisjunctiveFacet', '<=', 4)
- .toggleTag('tag1')
- .toggleTag('tag2');
-
- const createURL = () => '#cleared';
-
- initParameters = {
- helper,
- createURL,
- instantSearchInstance: {
- templatesConfig: { randomAttributeNeverUsed: 'value' },
- },
- };
-
- renderParameters = {
- results: {
- facets: [
- {
- name: 'facet',
- exhaustive: true,
- data: {
- 'facet-val1': 1,
- 'facet-val2': 2,
- 'facet-val3': 42,
- },
- },
- {
- name: 'extraFacet',
- exhaustive: true,
- data: {
- 'extraFacet-val1': 42,
- 'extraFacet-val2': 42,
- },
- },
- ],
- disjunctiveFacets: [
- {
- name: 'disjunctiveFacet',
- exhaustive: true,
- data: {
- 'disjunctiveFacet-val1': 3,
- 'disjunctiveFacet-val2': 4,
- 'disjunctiveFacet-val3': 42,
- },
- },
- ],
- hierarchicalFacets: [
- {
- name: 'hierarchicalFacet',
- data: [
- {
- name: 'hierarchicalFacet-val1',
- count: 5,
- exhaustive: true,
- data: [
- {
- name: 'hierarchicalFacet-val2',
- count: 6,
- exhaustive: true,
- },
- ],
- },
- {
- // Here to confirm we're taking the right nested one
- name: 'hierarchicalFacet-val2',
- count: 42,
- exhaustive: true,
- },
- ],
- },
- ],
- },
- helper,
- state: helper.state,
- templatesConfig: { randomAttributeNeverUsed: 'value' },
- createURL,
- };
-
- refinements = [
- {
- type: 'facet',
- attributeName: 'facet',
- name: 'facet-val1',
- count: 1,
- exhaustive: true,
- },
- {
- type: 'facet',
- attributeName: 'facet',
- name: 'facet-val2',
- count: 2,
- exhaustive: true,
- },
- {
- type: 'exclude',
- attributeName: 'facetExclude',
- name: 'facetExclude-val1',
- exclude: true,
- },
- {
- type: 'exclude',
- attributeName: 'facetExclude',
- name: 'facetExclude-val2',
- exclude: true,
- },
- // eslint-disable-next-line max-len
- {
- type: 'disjunctive',
- attributeName: 'disjunctiveFacet',
- name: 'disjunctiveFacet-val1',
- count: 3,
- exhaustive: true,
- },
- // eslint-disable-next-line max-len
- {
- type: 'disjunctive',
- attributeName: 'disjunctiveFacet',
- name: 'disjunctiveFacet-val2',
- count: 4,
- exhaustive: true,
- },
- // eslint-disable-next-line max-len
- {
- type: 'hierarchical',
- attributeName: 'hierarchicalFacet',
- name: 'hierarchicalFacet-val2',
- count: 6,
- exhaustive: true,
- },
- {
- type: 'numeric',
- attributeName: 'numericFacet',
- name: '1',
- numericValue: 1,
- operator: '>=',
- },
- {
- type: 'numeric',
- attributeName: 'numericFacet',
- name: '2',
- numericValue: 2,
- operator: '<=',
- },
- {
- type: 'numeric',
- attributeName: 'numericDisjunctiveFacet',
- name: '3',
- numericValue: 3,
- operator: '>=',
- },
- {
- type: 'numeric',
- attributeName: 'numericDisjunctiveFacet',
- name: '4',
- numericValue: 4,
- operator: '<=',
- },
- { type: 'tag', attributeName: '_tags', name: 'tag1' },
- { type: 'tag', attributeName: '_tags', name: 'tag2' },
- ];
-
- expectedProps = {
- attributes: {
- facet: { name: 'facet' },
- facetExclude: { name: 'facetExclude' },
- disjunctiveFacet: { name: 'disjunctiveFacet' },
- hierarchicalFacet: { name: 'hierarchicalFacet' },
- numericFacet: { name: 'numericFacet' },
- numericDisjunctiveFacet: { name: 'numericDisjunctiveFacet' },
- _tags: { name: '_tags' },
- },
- clearAllClick: () => {},
- collapsible: false,
- clearAllPosition: 'after',
- clearAllURL: '#cleared',
- cssClasses: {
- root: 'ais-current-refined-values root-css-class',
- header: 'ais-current-refined-values--header header-css-class',
- body: 'ais-current-refined-values--body body-css-class',
- clearAll: 'ais-current-refined-values--clear-all clear-all-css-class',
- list: 'ais-current-refined-values--list list-css-class',
- item: 'ais-current-refined-values--item item-css-class',
- link: 'ais-current-refined-values--link link-css-class',
- count: 'ais-current-refined-values--count count-css-class',
- footer: 'ais-current-refined-values--footer footer-css-class',
- },
- shouldAutoHideContainer: false,
- templateProps: prepareTemplateProps({
- defaultTemplates,
- templatesConfig: { randomAttributeNeverUsed: 'value' },
- templates: {
- header: 'HEADER',
- item: 'ITEM',
- clearAll: 'CLEAR ALL',
- footer: 'FOOTER',
- },
- }),
- };
- setRefinementsInExpectedProps();
- });
-
- it('should render twice ', () => {
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
- widget.render(renderParameters);
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(2);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- expect(ReactDOM.render.mock.calls[0][1]).toBe(parameters.container);
- expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
- expect(ReactDOM.render.mock.calls[1][1]).toBe(parameters.container);
- });
-
- describe('options.container', () => {
- it('should render with a string container', () => {
- const element = document.createElement('div');
- element.id = 'testid';
- document.body.appendChild(element);
-
- parameters.container = '#testid';
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- expect(ReactDOM.render.mock.calls[0][1]).toBe(element);
- });
-
- it('should render with a HTMLElement container', () => {
- const element = document.createElement('div');
-
- parameters.container = element;
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- expect(ReactDOM.render.mock.calls[0][1]).toBe(element);
- });
- });
-
- describe('options.attributes', () => {
- describe('with options.onlyListedAttributes === true', () => {
- beforeEach(() => {
- parameters.onlyListedAttributes = true;
- });
-
- it('should render all attributes with not defined attributes', () => {
- delete parameters.attributes;
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {};
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should render all attributes with an empty array', () => {
- parameters.attributes = [];
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {};
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should render and pass all attributes defined in each objects', () => {
- parameters.attributes = [
- {
- name: 'facet',
- },
- {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- ];
-
- refinements = refinements.filter(
- refinement =>
- ['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
- refinement.attributeName
- ) !== -1
- );
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {
- facet: {
- name: 'facet',
- },
- facetExclude: {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- disjunctiveFacet: {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- };
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('with options.onlyListedAttributes === false', () => {
- beforeEach(() => {
- parameters.onlyListedAttributes = false;
- });
-
- it('should render all attributes with not defined attributes', () => {
- delete parameters.attributes;
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {};
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should render all attributes with an empty array', () => {
- parameters.attributes = [];
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {};
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should render and pass all attributes defined in each objects', () => {
- parameters.attributes = [
- {
- name: 'facet',
- },
- {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- ];
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
- const firstRefinements = refinements.filter(
- refinement =>
- ['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
- refinement.attributeName
- ) !== -1
- );
- const otherRefinements = refinements.filter(
- refinement =>
- ['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
- refinement.attributeName
- ) === -1
- );
- refinements = [].concat(firstRefinements).concat(otherRefinements);
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {
- facet: {
- name: 'facet',
- },
- facetExclude: {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- disjunctiveFacet: {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- };
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('with options.onlyListedAttributes not defined', () => {
- beforeEach(() => {
- delete parameters.onlyListedAttributes;
- });
-
- it('should default to false', () => {
- parameters.attributes = [
- {
- name: 'facet',
- },
- {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- ];
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
- const firstRefinements = refinements.filter(
- refinement =>
- ['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
- refinement.attributeName
- ) !== -1
- );
- const otherRefinements = refinements.filter(
- refinement =>
- ['facet', 'facetExclude', 'disjunctiveFacet'].indexOf(
- refinement.attributeName
- ) === -1
- );
- refinements = [].concat(firstRefinements).concat(otherRefinements);
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {
- facet: {
- name: 'facet',
- },
- facetExclude: {
- name: 'facetExclude',
- label: 'Facet exclude',
- },
- disjunctiveFacet: {
- name: 'disjunctiveFacet',
- transformData: data => {
- data.name = 'newname';
- return data;
- },
- },
- };
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
- });
-
- describe('options.clearAll', () => {
- it('should pass it as clearAllPosition', () => {
- parameters.clearAll = 'before';
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.clearAllPosition = 'before';
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('options.templates', () => {
- it('should pass it in templateProps', () => {
- parameters.templates.item = 'MY CUSTOM TEMPLATE';
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.templateProps.templates.item = 'MY CUSTOM TEMPLATE';
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('options.transformItems', () => {
- it('should transform passed items', () => {
- const widget = currentRefinedValues({
- ...parameters,
- transformItems: items =>
- items.map(item => ({ ...item, transformed: true })),
- });
-
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('options.autoHideContainer', () => {
- describe('without refinements', () => {
- beforeEach(() => {
- helper.clearRefinements().clearTags();
- renderParameters.state = helper.state;
-
- expectedProps.refinements = [];
- expectedProps.clearRefinementClicks = [];
- expectedProps.clearRefinementURLs = [];
- expectedProps.shouldAutoHideContainer = true;
- });
-
- it('shouldAutoHideContainer should be true with autoHideContainer = true', () => {
- parameters.autoHideContainer = true;
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('shouldAutoHideContainer should be false with autoHideContainer = false', () => {
- parameters.autoHideContainer = false;
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters); // eslint-disable-next-line max-len
-
- widget.render(renderParameters);
-
- expectedProps.shouldAutoHideContainer = false;
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('with refinements', () => {
- it('shouldAutoHideContainer should be false with autoHideContainer = true', () => {
- parameters.autoHideContainer = true;
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.shouldAutoHideContainer = false;
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('shouldAutoHideContainer should be false with autoHideContainer = false', () => {
- parameters.autoHideContainer = false;
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.shouldAutoHideContainer = false;
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
- });
-
- describe('options.cssClasses', () => {
- it('should be passed in the cssClasses', () => {
- parameters.cssClasses.body = 'custom-passed-body';
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.cssClasses.body =
- 'ais-current-refined-values--body custom-passed-body';
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should work with an array', () => {
- parameters.cssClasses.body = ['custom-body', 'custom-body-2'];
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- expectedProps.cssClasses.body =
- 'ais-current-refined-values--body custom-body custom-body-2';
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- describe('with attributes', () => {
- it('should sort the refinements according to their order', () => {
- parameters.attributes = [
- { name: 'disjunctiveFacet' },
- { name: 'facetExclude' },
- ];
- parameters.onlyListedAttributes = false;
-
- refinements.splice(0, 0, {
- type: 'facet',
- attributeName: 'extraFacet',
- name: 'extraFacet-val1',
- count: 42,
- exhaustive: true,
- });
- const firstRefinements = refinements.filter(
- refinement => refinement.attributeName === 'disjunctiveFacet'
- );
- const secondRefinements = refinements.filter(
- refinement => refinement.attributeName === 'facetExclude'
- );
- const otherRefinements = refinements.filter(
- refinement =>
- !['disjunctiveFacet', 'facetExclude'].includes(
- refinement.attributeName
- )
- );
-
- refinements = []
- .concat(firstRefinements)
- .concat(secondRefinements)
- .concat(otherRefinements);
-
- const widget = currentRefinedValues(parameters);
- widget.init(initParameters);
- widget.render(renderParameters);
-
- setRefinementsInExpectedProps();
- expectedProps.attributes = {
- disjunctiveFacet: { name: 'disjunctiveFacet' },
- facetExclude: { name: 'facetExclude' },
- };
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
- });
-
- afterEach(() => {
- currentRefinedValues.__ResetDependency__('render');
- });
- });
-});
diff --git a/src/widgets/current-refined-values/__tests__/defaultTemplates-test.js b/src/widgets/current-refined-values/__tests__/defaultTemplates-test.js
deleted file mode 100644
index cfc94fa776..0000000000
--- a/src/widgets/current-refined-values/__tests__/defaultTemplates-test.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import defaultTemplates from '../defaultTemplates.js';
-
-describe('current-refined-values defaultTemplates', () => {
- describe('`item` template', () => {
- it('has a `item` default template', () => {
- const item = {
- type: 'disjunction',
- label: 'Brand',
- operator: ':',
- name: 'Samsung',
- count: 4,
- cssClasses: {
- count: 'ais-current-refined-values--count',
- },
- };
- expect(defaultTemplates.item(item)).toContain(
- '4 '
- );
- expect(defaultTemplates.item(item)).toMatchSnapshot();
- });
- it('wraps query refinements with ', () => {
- const item = {
- type: 'query',
- label: 'Query',
- operator: ':',
- name: 'Samsu',
- };
- expect(defaultTemplates.item(item)).toContain('Query : Samsu ');
- expect(defaultTemplates.item(item)).toMatchSnapshot();
- });
- it('does not show `count` when query refinement', () => {
- const item = {
- type: 'query',
- label: 'Query',
- operator: ':',
- name: 'Samsu',
- count: 22,
- };
- expect(defaultTemplates.item(item)).not.toContain(22);
- expect(defaultTemplates.item(item)).toMatchSnapshot();
- });
- });
-});
diff --git a/src/widgets/current-refined-values/current-refined-values.js b/src/widgets/current-refined-values/current-refined-values.js
deleted file mode 100644
index bc02ff0d5c..0000000000
--- a/src/widgets/current-refined-values/current-refined-values.js
+++ /dev/null
@@ -1,289 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import isUndefined from 'lodash/isUndefined';
-import isBoolean from 'lodash/isBoolean';
-import isString from 'lodash/isString';
-import isArray from 'lodash/isArray';
-import isPlainObject from 'lodash/isPlainObject';
-import isFunction from 'lodash/isFunction';
-import reduce from 'lodash/reduce';
-
-import CurrentRefinedValuesWithHOCs from '../../components/CurrentRefinedValues/CurrentRefinedValues.js';
-import connectCurrentRefinedValues from '../../connectors/current-refined-values/connectCurrentRefinedValues.js';
-import defaultTemplates from './defaultTemplates';
-
-import {
- isDomElement,
- bemHelper,
- getContainerNode,
- prepareTemplateProps,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-current-refined-values');
-
-const renderer = ({
- autoHideContainer,
- clearAllPosition,
- collapsible,
- containerNode,
- cssClasses,
- renderState,
- transformData,
- templates,
-}) => (
- {
- attributes,
- clearAllClick,
- clearAllURL,
- refine,
- createURL,
- refinements,
- instantSearchInstance,
- },
- isFirstRendering
-) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- transformData,
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- const shouldAutoHideContainer =
- autoHideContainer && refinements && refinements.length === 0;
-
- const clearRefinementClicks = refinements.map(refinement =>
- refine.bind(null, refinement)
- );
- const clearRefinementURLs = refinements.map(refinement =>
- createURL(refinement)
- );
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-currentRefinedValues({
- container,
- [ attributes: [{name[, label, template, transformData]}] ],
- [ onlyListedAttributes = false ],
- [ clearAll = 'before' ] // One of ['before', 'after', false]
- [ templates.{header,item,clearAll,footer} ],
- [ transformData.{item} ],
- [ autoHideContainer = true ],
- [ cssClasses.{root, header, body, clearAll, list, item, link, count, footer} = {} ],
- [ collapsible = false ],
- [ clearsQuery = false ],
- [ transformItems ]
-})`;
-
-/**
- * @typedef {Object} CurrentRefinedValuesCSSClasses
- * @property {string} [root] CSS classes added to the root element.
- * @property {string} [header] CSS classes added to the header element.
- * @property {string} [body] CSS classes added to the body element.
- * @property {string} [clearAll] CSS classes added to the clearAll element.
- * @property {string} [list] CSS classes added to the list element.
- * @property {string} [item] CSS classes added to the item element.
- * @property {string} [link] CSS classes added to the link element.
- * @property {string} [count] CSS classes added to the count element.
- * @property {string} [footer] CSS classes added to the footer element.
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesAttributes
- * @property {string} name Required attribute name.
- * @property {string} label Attribute label (passed to the item template).
- * @property {string|function(object):string} template Attribute specific template.
- * @property {function(object):object} transformData Attribute specific transformData.
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesTemplates
- * @property {string|function(object):string} [header] Header template.
- * @property {string|function(object):string} [item] Item template.
- * @property {string|function(object):string} [clearAll] Clear all template.
- * @property {string|function(object):string} [footer] Footer template.
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesTransforms
- * @property {function(object):object} [item] Method to change the object passed to the `item` template.
- */
-
-/**
- * @typedef {Object} CurrentRefinedValuesWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget
- * @property {CurrentRefinedValuesAttributes[]} [attributes = []] Label definitions for the
- * different filters.
- * @property {boolean} [onlyListedAttributes=false] Only use the declared attributes. By default, the widget
- * displays the refinements for the whole search state. If true, the list of attributes in `attributes` is used.
- * @property {'before'|'after'|boolean} [clearAll='before'] Defines the clear all button position.
- * By default, it is placed before the set of current filters. If the value is false, the button
- * won't be added in the widget.
- * @property {CurrentRefinedValuesTemplates} [templates] Templates to use for the widget.
- * @property {CurrentRefinedValuesTransforms} [transformData] Set of functions to transform
- * the data passed to the templates.
- * @property {boolean} [autoHideContainer=true] Hides the widget when there are no current refinements.
- * @property {CurrentRefinedValuesCSSClasses} [cssClasses] CSS classes to be added.
- * @property {boolean|{collapsed: boolean}} [collapsible=false] Makes the widget collapsible. The user can then
- * choose to hide the content of the widget. This option can also be an object with the property collapsed. If this
- * property is `true`, then the widget is hidden during the first rendering.
- * @property {boolean} [clearsQuery=false] If true, the clear all button also clears the active search query.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * The current refined values widget has two purposes:
- *
- * - give the user a synthetic view of the current filters.
- * - give the user the ability to remove a filter.
- *
- * This widget is usually in the top part of the search UI.
- * @type {WidgetFactory}
- * @devNovel CurrentRefinedValues
- * @category clear-filter
- * @param {CurrentRefinedValuesWidgetOptions} $0 The CurrentRefinedValues widget options.
- * @returns {Object} A new CurrentRefinedValues widget instance.
- * @example
- * search.addWidget(
- * instantsearch.widgets.currentRefinedValues({
- * container: '#current-refined-values',
- * clearAll: 'after',
- * clearsQuery: true,
- * attributes: [
- * {name: 'free_shipping', label: 'Free shipping'},
- * {name: 'price', label: 'Price'},
- * {name: 'brand', label: 'Brand'},
- * {name: 'category', label: 'Category'},
- * ],
- * onlyListedAttributes: true,
- * })
- * );
- */
-export default function currentRefinedValues({
- container,
- attributes = [],
- onlyListedAttributes = false,
- clearAll = 'before',
- templates = defaultTemplates,
- transformData,
- autoHideContainer = true,
- cssClasses: userCssClasses = {},
- collapsible = false,
- clearsQuery = false,
- transformItems,
-}) {
- const transformDataOK =
- isUndefined(transformData) ||
- isFunction(transformData) ||
- (isPlainObject(transformData) && isFunction(transformData.item));
-
- const templatesKeys = ['header', 'item', 'clearAll', 'footer'];
- const templatesOK =
- isPlainObject(templates) &&
- reduce(
- templates,
- (res, val, key) =>
- res &&
- templatesKeys.indexOf(key) !== -1 &&
- (isString(val) || isFunction(val)),
- true
- );
-
- const userCssClassesKeys = [
- 'root',
- 'header',
- 'body',
- 'clearAll',
- 'list',
- 'item',
- 'link',
- 'count',
- 'footer',
- ];
- const userCssClassesOK =
- isPlainObject(userCssClasses) &&
- reduce(
- userCssClasses,
- (res, val, key) =>
- (res && userCssClassesKeys.indexOf(key) !== -1 && isString(val)) ||
- isArray(val),
- true
- );
-
- const showUsage =
- false ||
- !(isString(container) || isDomElement(container)) ||
- !isArray(attributes) ||
- !isBoolean(onlyListedAttributes) ||
- [false, 'before', 'after'].indexOf(clearAll) === -1 ||
- !isPlainObject(templates) ||
- !templatesOK ||
- !transformDataOK ||
- !isBoolean(autoHideContainer) ||
- !userCssClassesOK;
-
- if (showUsage) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- clearAll: cx(bem('clear-all'), userCssClasses.clearAll),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- link: cx(bem('link'), userCssClasses.link),
- count: cx(bem('count'), userCssClasses.count),
- footer: cx(bem('footer'), userCssClasses.footer),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- clearAllPosition: clearAll,
- collapsible,
- cssClasses,
- autoHideContainer,
- renderState: {},
- templates,
- transformData,
- });
-
- try {
- const makeCurrentRefinedValues = connectCurrentRefinedValues(
- specializedRenderer,
- () => unmountComponentAtNode(containerNode)
- );
- return makeCurrentRefinedValues({
- attributes,
- onlyListedAttributes,
- clearAll,
- clearsQuery,
- transformItems,
- });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/current-refined-values/defaultTemplates.js b/src/widgets/current-refined-values/defaultTemplates.js
deleted file mode 100644
index d4b76fbb6f..0000000000
--- a/src/widgets/current-refined-values/defaultTemplates.js
+++ /dev/null
@@ -1,30 +0,0 @@
-export default {
- header: '',
- item: itemTemplate,
- clearAll: 'Clear all',
- footer: '',
-};
-
-function itemTemplate({
- type,
- label,
- operator,
- displayOperator,
- exclude,
- name,
- count,
- cssClasses,
-}) {
- const computedOperator = operator ? displayOperator : '';
- const computedLabel = label
- ? `${label} ${computedOperator || ':'} `
- : computedOperator;
- const countValue = count === undefined ? 0 : count;
- const computedCount =
- type === 'query'
- ? ''
- : `${countValue} `;
- const computedExclude = exclude ? '-' : '';
- const computedName = type === 'query' ? `${name} ` : name;
- return `${computedLabel} ${computedExclude} ${computedName} ${computedCount}`;
-}
diff --git a/src/widgets/current-refinements/__tests__/__snapshots__/current-refinements-test.js.snap b/src/widgets/current-refinements/__tests__/__snapshots__/current-refinements-test.js.snap
new file mode 100644
index 0000000000..d7590ea673
--- /dev/null
+++ b/src/widgets/current-refinements/__tests__/__snapshots__/current-refinements-test.js.snap
@@ -0,0 +1,280 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`currentRefinements() render() DOM output renders correctly 1`] = `
+ hierarchicalFacet-val2",
+ "type": "hierarchical",
+ "value": "hierarchicalFacet-val1 > hierarchicalFacet-val2",
+ },
+ ],
+ },
+ Object {
+ "attribute": "numericFacet",
+ "refine": [Function],
+ "refinements": Array [
+ Object {
+ "attribute": "numericFacet",
+ "label": "≥ 1",
+ "operator": ">=",
+ "type": "numeric",
+ "value": 1,
+ },
+ Object {
+ "attribute": "numericFacet",
+ "label": "≤ 2",
+ "operator": "<=",
+ "type": "numeric",
+ "value": 2,
+ },
+ ],
+ },
+ Object {
+ "attribute": "numericDisjunctiveFacet",
+ "refine": [Function],
+ "refinements": Array [
+ Object {
+ "attribute": "numericDisjunctiveFacet",
+ "label": "≥ 3",
+ "operator": ">=",
+ "type": "numeric",
+ "value": 3,
+ },
+ Object {
+ "attribute": "numericDisjunctiveFacet",
+ "label": "≤ 4",
+ "operator": "<=",
+ "type": "numeric",
+ "value": 4,
+ },
+ ],
+ },
+ Object {
+ "attribute": "_tags",
+ "refine": [Function],
+ "refinements": Array [
+ Object {
+ "attribute": "_tags",
+ "label": "tag1",
+ "type": "tag",
+ "value": "tag1",
+ },
+ Object {
+ "attribute": "_tags",
+ "label": "tag2",
+ "type": "tag",
+ "value": "tag2",
+ },
+ ],
+ },
+ ]
+ }
+/>
+`;
+
+exports[`currentRefinements() render() options.container should render with a HTMLElement container 1`] = `
+
+`;
+
+exports[`currentRefinements() render() options.container should render with a string container 1`] = `
+
+`;
+
+exports[`currentRefinements() render() should render twice 1`] = `
+
+`;
+
+exports[`currentRefinements() render() should render twice 2`] = `
+
+`;
+
+exports[`currentRefinements() usage throws usage when no options provided 1`] = `
+"Usage:
+currentRefinements({
+ container,
+ [ includedAttributes ],
+ [ excludedAttributes = ['query'] ],
+ [ cssClasses.{root, list, item, label, category, categoryLabel, delete} ],
+ [ transformItems ]
+})"
+`;
diff --git a/src/widgets/current-refinements/__tests__/current-refinements-test.js b/src/widgets/current-refinements/__tests__/current-refinements-test.js
new file mode 100644
index 0000000000..223f96aedb
--- /dev/null
+++ b/src/widgets/current-refinements/__tests__/current-refinements-test.js
@@ -0,0 +1,618 @@
+import algoliasearch from 'algoliasearch';
+import algoliasearchHelper from 'algoliasearch-helper';
+import currentRefinements from '../current-refinements';
+
+describe('currentRefinements()', () => {
+ beforeEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ describe('usage', () => {
+ it('throws usage when no options provided', () => {
+ expect(currentRefinements.bind(null, {})).toThrowErrorMatchingSnapshot();
+ });
+ });
+
+ describe('types checking', () => {
+ describe('options.container', () => {
+ it('does not throw with a string', () => {
+ const container = document.createElement('div');
+ container.id = 'container';
+ document.body.append(container);
+
+ expect(
+ currentRefinements.bind(null, {
+ container: '#container',
+ })
+ ).not.toThrow();
+ });
+
+ it('does not throw with a HTMLElement', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ })
+ ).not.toThrow();
+ });
+ });
+
+ describe('options.includedAttributes', () => {
+ it('does not throw with array of strings', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ includedAttributes: ['attr1', 'attr2', 'attr3', 'attr14'],
+ })
+ ).not.toThrow();
+ });
+ });
+
+ describe('options.templates', () => {
+ it('does not throw with an empty object', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ templates: {},
+ })
+ ).not.toThrow();
+ });
+
+ it('does not throw with a string template', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ templates: {
+ item: 'string template',
+ },
+ })
+ ).not.toThrow();
+ });
+
+ it('does not throw with a function template', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ templates: {
+ item: () => 'function template',
+ },
+ })
+ ).not.toThrow();
+ });
+ });
+
+ describe('options.cssClasses', () => {
+ it('does not throw with an empty object', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ cssClasses: {},
+ })
+ ).not.toThrow();
+ });
+
+ it('does not throw with string class', () => {
+ expect(
+ currentRefinements.bind(null, {
+ container: document.createElement('div'),
+ cssClasses: {
+ item: 'itemClass',
+ },
+ })
+ ).not.toThrow();
+ });
+ });
+ });
+
+ describe('getConfiguration()', () => {
+ it('configures nothing', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ });
+ expect(widget.getConfiguration).toEqual(undefined);
+ });
+ });
+
+ describe('render()', () => {
+ let ReactDOM;
+ let client;
+ let helper;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ currentRefinements.__Rewire__('render', ReactDOM.render);
+
+ client = algoliasearch('APP_ID', 'API_KEY');
+ helper = algoliasearchHelper(client, 'index_name', {
+ facets: ['facet', 'facetExclude', 'numericFacet', 'extraFacet'],
+ disjunctiveFacets: ['disjunctiveFacet', 'numericDisjunctiveFacet'],
+ hierarchicalFacets: [
+ {
+ name: 'hierarchicalFacet',
+ attributes: ['hierarchicalFacet-val1', 'hierarchicalFacet-val2'],
+ separator: ' > ',
+ },
+ ],
+ });
+ });
+
+ afterEach(() => {
+ currentRefinements.__ResetDependency__('render');
+ });
+
+ it('should render twice ', () => {
+ const container = document.createElement('div');
+ const widget = currentRefinements({
+ container,
+ });
+
+ helper.addFacetRefinement('facet', 'facet-val1');
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ const renderParameters = {
+ results: {
+ facets: [
+ {
+ name: 'facet',
+ data: {
+ 'facet-val1': 1,
+ },
+ },
+ ],
+ },
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ };
+
+ widget.render(renderParameters);
+ widget.render(renderParameters);
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toBe(container);
+ });
+
+ describe('options.container', () => {
+ it('should render with a string container', () => {
+ const container = document.createElement('div');
+ container.id = 'container';
+ document.body.appendChild(container);
+
+ const widget = currentRefinements({
+ container: '#container',
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {},
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(container);
+ });
+
+ it('should render with a HTMLElement container', () => {
+ const container = document.createElement('div');
+ const widget = currentRefinements({
+ container,
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {},
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toBe(container);
+ });
+ });
+
+ describe('options.includedAttributes', () => {
+ it('should only include the specified attributes', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ includedAttributes: ['disjunctiveFacet'],
+ });
+
+ helper
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val1'
+ )
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val2'
+ )
+ // Add some unused refinements to make sure they're ignored
+ .addFacetRefinement('facet', 'facet-val1')
+ .addFacetRefinement('facet', 'facet-val2')
+ .addFacetRefinement('extraFacet', 'extraFacet-val1')
+ .addFacetRefinement('extraFacet', 'extraFacet-val2');
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {
+ facets: [
+ {
+ name: 'facet',
+ exhaustive: true,
+ data: {
+ 'facet-val1': 1,
+ 'facet-val2': 2,
+ },
+ },
+ {
+ name: 'extraFacet',
+ exhaustive: true,
+ data: {
+ 'extraFacet-val1': 42,
+ 'extraFacet-val2': 42,
+ },
+ },
+ ],
+ disjunctiveFacets: [
+ {
+ name: 'disjunctiveFacet',
+ exhaustive: true,
+ data: {
+ 'disjunctiveFacet-val1': 3,
+ 'disjunctiveFacet-val2': 4,
+ },
+ },
+ ],
+ },
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ const renderedItems = ReactDOM.render.mock.calls[0][0].props.items;
+ expect(renderedItems).toHaveLength(1);
+
+ const [item] = renderedItems;
+ expect(item.attribute).toBe('disjunctiveFacet');
+ expect(item.refinements).toHaveLength(2);
+ });
+
+ it('should ignore all attributes when empty array', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ includedAttributes: [],
+ });
+
+ helper
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val1'
+ )
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val2'
+ )
+ .addFacetRefinement('extraFacet', 'extraFacet-val1')
+ .addFacetRefinement('extraFacet', 'extraFacet-val2');
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {
+ facets: [
+ {
+ name: 'extraFacet',
+ exhaustive: true,
+ data: {
+ 'extraFacet-val1': 42,
+ 'extraFacet-val2': 42,
+ },
+ },
+ ],
+ disjunctiveFacets: [
+ {
+ name: 'disjunctiveFacet',
+ exhaustive: true,
+ data: {
+ 'disjunctiveFacet-val1': 3,
+ 'disjunctiveFacet-val2': 4,
+ },
+ },
+ ],
+ },
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ const renderedItems = ReactDOM.render.mock.calls[0][0].props.items;
+ expect(renderedItems).toHaveLength(0);
+ });
+ });
+
+ describe('options.excludedAttributes', () => {});
+
+ describe('options.transformItems', () => {
+ it('should transform passed items', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ transformItems: items =>
+ items.map(refinementItems => ({
+ ...refinementItems,
+ refinements: refinementItems.refinements.map(item => ({
+ ...item,
+ transformed: true,
+ })),
+ })),
+ });
+
+ helper
+ .addFacetRefinement('facet', 'facet-val1')
+ .addFacetRefinement('facet', 'facet-val2')
+ .addFacetRefinement('extraFacet', 'facet-val1')
+ .addFacetRefinement('extraFacet', 'facet-val2')
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val1'
+ )
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val2'
+ );
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {
+ facets: [
+ {
+ name: 'facet',
+ exhaustive: true,
+ data: {
+ 'facet-val1': 1,
+ 'facet-val2': 2,
+ },
+ },
+ {
+ name: 'extraFacet',
+ exhaustive: true,
+ data: {
+ 'extraFacet-val1': 42,
+ 'extraFacet-val2': 42,
+ },
+ },
+ ],
+ disjunctiveFacets: [
+ {
+ name: 'disjunctiveFacet',
+ exhaustive: true,
+ data: {
+ 'disjunctiveFacet-val1': 3,
+ 'disjunctiveFacet-val2': 4,
+ },
+ },
+ ],
+ },
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ const renderedItems = ReactDOM.render.mock.calls[0][0].props.items;
+
+ expect(renderedItems[0].refinements[0].transformed).toBe(true);
+ expect(renderedItems[0].refinements[1].transformed).toBe(true);
+ expect(renderedItems[1].refinements[0].transformed).toBe(true);
+ expect(renderedItems[1].refinements[1].transformed).toBe(true);
+ expect(renderedItems[2].refinements[0].transformed).toBe(true);
+ expect(renderedItems[2].refinements[1].transformed).toBe(true);
+ });
+ });
+
+ describe('options.cssClasses', () => {
+ it('should be passed in the cssClasses', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ cssClasses: {
+ root: 'customRoot',
+ },
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {},
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ expect(
+ ReactDOM.render.mock.calls[0][0].props.cssClasses.root
+ ).toContain('customRoot');
+ });
+
+ it('should work with an array', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ cssClasses: {
+ root: ['customRoot1', 'customRoot2'],
+ },
+ });
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {},
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ expect(
+ ReactDOM.render.mock.calls[0][0].props.cssClasses.root
+ ).toContain('customRoot1');
+ expect(
+ ReactDOM.render.mock.calls[0][0].props.cssClasses.root
+ ).toContain('customRoot2');
+ });
+ });
+
+ describe('DOM output', () => {
+ it('renders correctly', () => {
+ const widget = currentRefinements({
+ container: document.createElement('div'),
+ cssClasses: {
+ root: 'root',
+ list: 'list',
+ item: 'item',
+ label: 'label',
+ category: 'category',
+ categoryLabel: 'categoryLabel',
+ delete: 'delete',
+ query: 'query',
+ },
+ });
+
+ helper
+ .addFacetRefinement('facet', 'facet-val1')
+ .addFacetRefinement('facet', 'facet-val2')
+ .addFacetRefinement('extraFacet', 'extraFacet-val1')
+ .addFacetExclusion('facetExclude', 'facetExclude-val1')
+ .addFacetExclusion('facetExclude', 'facetExclude-val2')
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val1'
+ )
+ .addDisjunctiveFacetRefinement(
+ 'disjunctiveFacet',
+ 'disjunctiveFacet-val2'
+ )
+ .toggleFacetRefinement(
+ 'hierarchicalFacet',
+ 'hierarchicalFacet-val1 > hierarchicalFacet-val2'
+ )
+ .addNumericRefinement('numericFacet', '>=', 1)
+ .addNumericRefinement('numericFacet', '<=', 2)
+ .addNumericRefinement('numericDisjunctiveFacet', '>=', 3)
+ .addNumericRefinement('numericDisjunctiveFacet', '<=', 4)
+ .addTag('tag1')
+ .addTag('tag2');
+
+ widget.init({
+ helper,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+ widget.render({
+ results: {
+ facets: [
+ {
+ name: 'facet',
+ exhaustive: true,
+ data: {
+ 'facet-val1': 1,
+ 'facet-val2': 2,
+ 'facet-val3': 42,
+ },
+ },
+ {
+ name: 'extraFacet',
+ exhaustive: true,
+ data: {
+ 'extraFacet-val1': 42,
+ 'extraFacet-val2': 42,
+ },
+ },
+ ],
+ disjunctiveFacets: [
+ {
+ name: 'disjunctiveFacet',
+ exhaustive: true,
+ data: {
+ 'disjunctiveFacet-val1': 3,
+ 'disjunctiveFacet-val2': 4,
+ 'disjunctiveFacet-val3': 42,
+ },
+ },
+ ],
+ hierarchicalFacets: [
+ {
+ name: 'hierarchicalFacet',
+ data: [
+ {
+ name: 'hierarchicalFacet-val1',
+ count: 5,
+ exhaustive: true,
+ data: [
+ {
+ name: 'hierarchicalFacet-val2',
+ count: 6,
+ exhaustive: true,
+ },
+ ],
+ },
+ {
+ name: 'hierarchicalFacet-val2',
+ count: 42,
+ exhaustive: true,
+ },
+ ],
+ },
+ ],
+ },
+ helper,
+ state: helper.state,
+ createURL: () => '#cleared',
+ instantSearchInstance: {},
+ });
+
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/widgets/current-refinements/current-refinements.js b/src/widgets/current-refinements/current-refinements.js
new file mode 100644
index 0000000000..edeb53d39f
--- /dev/null
+++ b/src/widgets/current-refinements/current-refinements.js
@@ -0,0 +1,119 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import CurrentRefinements from '../../components/CurrentRefinements/CurrentRefinements.js';
+import connectCurrentRefinements from '../../connectors/current-refinements/connectCurrentRefinements.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('CurrentRefinements');
+
+const renderer = ({ containerNode, cssClasses }) => (
+ { items },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ return;
+ }
+
+ render(
+ ,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+currentRefinements({
+ container,
+ [ includedAttributes ],
+ [ excludedAttributes = ['query'] ],
+ [ cssClasses.{root, list, item, label, category, categoryLabel, delete} ],
+ [ transformItems ]
+})`;
+
+/**
+ * @typedef {Object} CurrentRefinementsCSSClasses
+ * @property {string} [root] CSS classes added to the root element.
+ * @property {string} [list] CSS classes added to the list element.
+ * @property {string} [item] CSS classes added to the item element.
+ * @property {string} [label] CSS classes added to the label element.
+ * @property {string} [category] CSS classes added to the category element.
+ * @property {string} [categoryLabel] CSS classes added to the categoryLabel element.
+ * @property {string} [delete] CSS classes added to the delete element.
+ */
+
+/**
+ * @typedef {Object} CurrentRefinementsWidgetOptions
+ * @property {string|HTMLElement} container The CSS Selector or HTMLElement to insert the widget
+ * @property {string[]} [includedAttributes] The attributes to include in the refinements (all by default)
+ * @property {string[]} [excludedAttributes = ['query']] The attributes to exclude from the refinements
+ * @property {CurrentRefinementsCSSClasses} [cssClasses] The CSS classes to be added
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * The `currentRefinements` widget has two purposes give the user a synthetic view of the current filters
+ * and the ability to remove a filter.
+ *
+ * This widget is usually in the top part of the search UI.
+ * @type {WidgetFactory}
+ * @devNovel CurrentRefinements
+ * @category clear-filter
+ * @param {CurrentRefinementsWidgetOptions} $0 The CurrentRefinements widget options.
+ * @returns {Object} A new CurrentRefinements widget instance.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.currentRefinements({
+ * container: '#current-refinements',
+ * includedAttributes: [
+ * 'free_shipping',
+ * 'price',
+ * 'brand',
+ * 'category',
+ * ],
+ * })
+ * );
+ */
+export default function currentRefinements({
+ container,
+ includedAttributes,
+ excludedAttributes,
+ cssClasses: userCssClasses = {},
+ transformItems,
+}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ category: cx(suit({ descendantName: 'category' }), userCssClasses.category),
+ categoryLabel: cx(
+ suit({ descendantName: 'categoryLabel' }),
+ userCssClasses.categoryLabel
+ ),
+ delete: cx(suit({ descendantName: 'delete' }), userCssClasses.delete),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ });
+
+ try {
+ const makeWidget = connectCurrentRefinements(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+
+ return makeWidget({
+ includedAttributes,
+ excludedAttributes,
+ transformItems,
+ });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/geo-search/GeoSearchRenderer.js b/src/widgets/geo-search/GeoSearchRenderer.js
index 1caa347560..f9be00c055 100644
--- a/src/widgets/geo-search/GeoSearchRenderer.js
+++ b/src/widgets/geo-search/GeoSearchRenderer.js
@@ -2,35 +2,17 @@ import React, { render } from 'preact-compat';
import { prepareTemplateProps } from '../../lib/utils';
import GeoSearchControls from '../../components/GeoSearchControls/GeoSearchControls';
-const refineWithMap = ({ refine, paddingBoundingBox, mapInstance }) => {
- // Function for compute the projection of LatLng to Point (pixel)
- // Builtin in Leaflet: myMapInstance.project(LatLng, zoom)
- // http://krasimirtsonev.com/blog/article/google-maps-api-v3-convert-latlng-object-to-actual-pixels-point-object
- // http://leafletjs.com/reference-1.2.0.html#map-project
- const scale = Math.pow(2, mapInstance.getZoom());
-
- const northEastPoint = mapInstance
- .getProjection()
- .fromLatLngToPoint(mapInstance.getBounds().getNorthEast());
-
- northEastPoint.x = northEastPoint.x - paddingBoundingBox.right / scale;
- northEastPoint.y = northEastPoint.y + paddingBoundingBox.top / scale;
-
- const southWestPoint = mapInstance
- .getProjection()
- .fromLatLngToPoint(mapInstance.getBounds().getSouthWest());
-
- southWestPoint.x = southWestPoint.x + paddingBoundingBox.right / scale;
- southWestPoint.y = southWestPoint.y - paddingBoundingBox.bottom / scale;
-
- const ne = mapInstance.getProjection().fromPointToLatLng(northEastPoint);
- const sw = mapInstance.getProjection().fromPointToLatLng(southWestPoint);
-
+const refineWithMap = ({ refine, mapInstance }) =>
refine({
- northEast: { lat: ne.lat(), lng: ne.lng() },
- southWest: { lat: sw.lat(), lng: sw.lng() },
+ northEast: mapInstance
+ .getBounds()
+ .getNorthEast()
+ .toJSON(),
+ southWest: mapInstance
+ .getBounds()
+ .getSouthWest()
+ .toJSON(),
});
-};
const collectMarkersForNextRender = (markers, nextIds) =>
markers.reduce(
@@ -44,10 +26,29 @@ const collectMarkersForNextRender = (markers, nextIds) =>
[[], []]
);
+const createBoundingBoxFromMarkers = (google, markers) => {
+ const latLngBounds = markers.reduce(
+ (acc, marker) => acc.extend(marker.getPosition()),
+ new google.maps.LatLngBounds()
+ );
+
+ return {
+ northEast: latLngBounds.getNorthEast().toJSON(),
+ southWest: latLngBounds.getSouthWest().toJSON(),
+ };
+};
+
+const lockUserInteraction = (renderState, functionThatAltersTheMapPosition) => {
+ renderState.isUserInteraction = false;
+ functionThatAltersTheMapPosition();
+ renderState.isUserInteraction = true;
+};
+
const renderer = (
{
items,
position,
+ currentRefinement,
refine,
clearMapRefinement,
toggleRefineOnMapMove,
@@ -67,9 +68,9 @@ const renderer = (
templates,
initialZoom,
initialPosition,
+ enableRefine,
enableClearMapRefinement,
enableRefineControl,
- paddingBoundingBox,
mapOptions,
createMarker,
markerOptions,
@@ -89,9 +90,9 @@ const renderer = (
mapElement.className = cssClasses.map;
rootElement.appendChild(mapElement);
- const controlElement = document.createElement('div');
- controlElement.className = cssClasses.controls;
- rootElement.appendChild(controlElement);
+ const treeElement = document.createElement('div');
+ treeElement.className = cssClasses.tree;
+ rootElement.appendChild(treeElement);
renderState.mapInstance = new googleReference.maps.Map(mapElement, {
mapTypeControl: false,
@@ -106,7 +107,7 @@ const renderer = (
const setupListenersWhenMapIsReady = () => {
const onChange = () => {
- if (renderState.isUserInteraction) {
+ if (renderState.isUserInteraction && enableRefine) {
setMapMoveSinceLastRefine();
if (isRefineOnMapMove()) {
@@ -126,7 +127,6 @@ const renderer = (
refineWithMap({
mapInstance: renderState.mapInstance,
refine,
- paddingBoundingBox,
});
}
});
@@ -146,15 +146,6 @@ const renderer = (
return;
}
- if (!items.length && !isRefinedWithMap() && !hasMapMoveSinceLastRefine()) {
- const initialMapPosition = position || initialPosition;
-
- renderState.isUserInteraction = false;
- renderState.mapInstance.setCenter(initialMapPosition);
- renderState.mapInstance.setZoom(initialZoom);
- renderState.isUserInteraction = true;
- }
-
// Collect markers that need to be updated or removed
const nextItemsIds = items.map(_ => _.objectID);
const [updateMarkers, exitMarkers] = collectMarkersForNextRender(
@@ -194,29 +185,40 @@ const renderer = (
})
);
- // Fit the map to the markers when needed
- const hasMarkers = renderState.markers.length;
- const center = renderState.mapInstance.getCenter();
- const zoom = renderState.mapInstance.getZoom();
- const isPositionInitialize = center !== undefined && zoom !== undefined;
- const enableFitBounds =
- !hasMapMoveSinceLastRefine() &&
- (!isRefinedWithMap() || (isRefinedWithMap() && !isPositionInitialize));
-
- if (hasMarkers && enableFitBounds) {
- const bounds = renderState.markers.reduce(
- (acc, marker) => acc.extend(marker.getPosition()),
- new googleReference.maps.LatLngBounds()
- );
-
- renderState.isUserInteraction = false;
- renderState.mapInstance.fitBounds(bounds);
- renderState.isUserInteraction = true;
+ const shouldUpdate = !hasMapMoveSinceLastRefine();
+
+ // We use this value for differentiate the padding to apply during
+ // fitBounds. When we don't have a currenRefinement (boundingBox)
+ // we let Google Maps compute the automatic padding. But when we
+ // provide the currentRefinement we explicitly set the padding
+ // to `0` otherwise the map will decrease the zoom on each refine.
+ const boundingBoxPadding = currentRefinement ? 0 : null;
+ const boundingBox =
+ !currentRefinement && Boolean(renderState.markers.length)
+ ? createBoundingBoxFromMarkers(googleReference, renderState.markers)
+ : currentRefinement;
+
+ if (boundingBox && shouldUpdate) {
+ lockUserInteraction(renderState, () => {
+ renderState.mapInstance.fitBounds(
+ new googleReference.maps.LatLngBounds(
+ boundingBox.southWest,
+ boundingBox.northEast
+ ),
+ boundingBoxPadding
+ );
+ });
+ } else if (shouldUpdate) {
+ lockUserInteraction(renderState, () => {
+ renderState.mapInstance.setCenter(position || initialPosition);
+ renderState.mapInstance.setZoom(initialZoom);
+ });
}
render(
,
- container.querySelector(`.${cssClasses.controls}`)
+ container.querySelector(`.${cssClasses.tree}`)
);
};
diff --git a/src/widgets/geo-search/__tests__/__snapshots__/geo-search-test.js.snap b/src/widgets/geo-search/__tests__/__snapshots__/geo-search-test.js.snap
index e302780593..568f618f7f 100644
--- a/src/widgets/geo-search/__tests__/__snapshots__/geo-search-test.js.snap
+++ b/src/widgets/geo-search/__tests__/__snapshots__/geo-search-test.js.snap
@@ -1,24 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`GeoSearch expect to render with custom classNames 1`] = `""`;
+exports[`GeoSearch expect to render 1`] = `""`;
-exports[`GeoSearch expect to render with custom classNames 2`] = `
+exports[`GeoSearch expect to render 2`] = `
Array [
Your custom HTML Marker",
"redo": "Redo search here",
+ "reset": "Clear the map refinement",
"toggle": "Search as I move the map",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "clear": true,
+ "HTMLMarker": true,
"redo": true,
+ "reset": true,
"toggle": true,
},
}
}
/>,
- null,
+
,
]
`;
-exports[`GeoSearch expect to render 1`] = `""`;
+exports[`GeoSearch expect to render with custom classNames 1`] = `""`;
-exports[`GeoSearch expect to render 2`] = `
+exports[`GeoSearch expect to render with custom classNames 2`] = `
Array [
Your custom HTML Marker",
"redo": "Redo search here",
+ "reset": "Clear the map refinement",
"toggle": "Search as I move the map",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "clear": true,
+ "HTMLMarker": true,
"redo": true,
+ "reset": true,
"toggle": true,
},
}
}
/>,
,
]
`;
diff --git a/src/widgets/geo-search/__tests__/geo-search-test.js b/src/widgets/geo-search/__tests__/geo-search-test.js
index 5e2df6b7d8..47f576db63 100644
--- a/src/widgets/geo-search/__tests__/geo-search-test.js
+++ b/src/widgets/geo-search/__tests__/geo-search-test.js
@@ -30,17 +30,17 @@ describe('GeoSearch', () => {
getZoom: jest.fn(),
setZoom: jest.fn(),
getBounds: jest.fn(() => ({
- getNorthEast: jest.fn(),
- getSouthWest: jest.fn(),
- })),
- getProjection: jest.fn(() => ({
- fromPointToLatLng: jest.fn(() => ({
- lat: jest.fn(),
- lng: jest.fn(),
+ getNorthEast: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 10,
+ lng: 12,
+ })),
})),
- fromLatLngToPoint: jest.fn(() => ({
- x: 0,
- y: 0,
+ getSouthWest: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 12,
+ lng: 14,
+ })),
})),
})),
fitBounds: jest.fn(),
@@ -58,8 +58,22 @@ describe('GeoSearch', () => {
} = {}) => ({
maps: {
LatLng: jest.fn(),
- LatLngBounds: jest.fn(() => ({
+ LatLngBounds: jest.fn((southWest, northEast) => ({
+ northEast,
+ southWest,
extend: jest.fn().mockReturnThis(),
+ getNorthEast: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 10,
+ lng: 12,
+ })),
+ })),
+ getSouthWest: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 12,
+ lng: 14,
+ })),
+ })),
})),
Map: jest.fn(() => mapInstance),
Marker: jest.fn(args => ({
@@ -159,12 +173,13 @@ describe('GeoSearch', () => {
cssClasses: {
root: 'custom-root',
map: 'custom-map',
- controls: 'custom-controls',
- clear: 'custom-clear',
control: 'custom-control',
- toggleLabel: 'custom-toggleLabel',
- toggleInput: 'custom-toggleInput',
+ label: 'custom-label',
+ selectedLabel: 'custom-label-selected',
+ input: 'custom-input',
redo: 'custom-redo',
+ disabledRedo: 'custom-redo-disabled',
+ reset: 'custom-reset',
},
});
@@ -217,7 +232,8 @@ describe('GeoSearch', () => {
const actual = renderer.mock.calls[0][0].widgetParams.templates;
const expectation = {
- clear: 'Clear the map refinement',
+ HTMLMarker: 'Your custom HTML Marker
',
+ reset: 'Clear the map refinement',
toggle: 'Search when the map move',
redo: 'Redo search here',
};
@@ -225,46 +241,6 @@ describe('GeoSearch', () => {
expect(actual).toEqual(expectation);
});
- it('expect to render with custom paddingBoundingBoc', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const googleReference = createFakeGoogleReference();
-
- const widget = geoSearch({
- googleReference,
- container,
- paddingBoundingBox: {
- top: 10,
- },
- });
-
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [],
- },
- });
-
- const actual = renderer.mock.calls[0][0].widgetParams.paddingBoundingBox;
-
- const expectation = {
- top: 10,
- right: 0,
- bottom: 0,
- left: 0,
- };
-
- expect(actual).toEqual(expectation);
- });
-
it('expect to render the map with default options', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
@@ -686,236 +662,45 @@ describe('GeoSearch', () => {
);
expect(lastRenderState(renderer).isPendingRefine).toBe(false);
});
- });
- });
- describe('initial position', () => {
- it('expect to init the position from "initialPosition"', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const mapInstance = createFakeMapInstance();
- const googleReference = createFakeGoogleReference({ mapInstance });
-
- const widget = geoSearch({
- googleReference,
- container,
- initialZoom: 8,
- initialPosition: {
- lat: 10,
- lng: 12,
- },
- });
-
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [],
- },
- });
-
- expect(mapInstance.setCenter).toHaveBeenCalledWith({ lat: 10, lng: 12 });
- expect(mapInstance.setZoom).toHaveBeenCalledWith(8);
- });
-
- it('expect to init the position from "position"', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const mapInstance = createFakeMapInstance();
- const googleReference = createFakeGoogleReference({ mapInstance });
-
- const widget = geoSearch({
- googleReference,
- container,
- initialZoom: 8,
- position: {
- lat: 12,
- lng: 14,
- },
- initialPosition: {
- lat: 10,
- lng: 12,
- },
- });
-
- // Simulate the configuration for the position
- helper.setState(widget.getConfiguration({}));
-
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [],
- },
- });
-
- expect(mapInstance.setCenter).toHaveBeenCalledWith({ lat: 12, lng: 14 });
- expect(mapInstance.setZoom).toHaveBeenCalledWith(8);
- });
-
- it('expect to not init the position when items are available', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const mapInstance = createFakeMapInstance();
- const googleReference = createFakeGoogleReference({ mapInstance });
-
- const widget = geoSearch({
- googleReference,
- container,
- initialZoom: 8,
- initialPosition: {
- lat: 10,
- lng: 12,
- },
- });
-
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [{ objectID: 123, _geoloc: true }],
- },
- });
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
- });
-
- it('expect to not init the position when the refinement is from the map', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const mapInstance = createFakeMapInstance();
- const googleReference = createFakeGoogleReference({ mapInstance });
-
- const widget = geoSearch({
- googleReference,
- container,
- initialZoom: 8,
- initialPosition: {
- lat: 10,
- lng: 12,
- },
- });
-
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
-
- simulateMapReadyEvent(googleReference);
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [{ objectID: 123, _geoloc: true }],
- },
- });
-
- // Simulate a refinement
- simulateEvent(mapInstance, 'dragstart');
- simulateEvent(mapInstance, 'center_changed');
- simulateEvent(mapInstance, 'idle');
-
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [],
- },
- });
-
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
- });
-
- it('expect to not init the position when the map has moved', () => {
- const container = createContainer();
- const instantSearchInstance = createFakeInstantSearch();
- const helper = createFakeHelper();
- const mapInstance = createFakeMapInstance();
- const googleReference = createFakeGoogleReference({ mapInstance });
-
- const widget = geoSearch({
- googleReference,
- container,
- enableRefineOnMapMove: false,
- initialZoom: 8,
- initialPosition: {
- lat: 10,
- lng: 12,
- },
- });
+ it(`expect to listen for "${eventName}" and do not trigger when refine is disabled`, () => {
+ const container = createContainer();
+ const instantSearchInstance = createFakeInstantSearch();
+ const helper = createFakeHelper();
+ const mapInstance = createFakeMapInstance();
+ const googleReference = createFakeGoogleReference({ mapInstance });
- widget.init({
- helper,
- instantSearchInstance,
- state: helper.state,
- });
+ const widget = geoSearch({
+ googleReference,
+ container,
+ enableRefine: false,
+ });
- simulateMapReadyEvent(googleReference);
+ widget.init({
+ helper,
+ instantSearchInstance,
+ state: helper.state,
+ });
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
+ simulateMapReadyEvent(googleReference);
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [{ objectID: 123, _geoloc: true }],
- },
- });
+ expect(mapInstance.addListener).toHaveBeenCalledWith(
+ eventName,
+ expect.any(Function)
+ );
- // Simulate a refinement
- simulateEvent(mapInstance, 'dragstart');
- simulateEvent(mapInstance, 'center_changed');
- simulateEvent(mapInstance, 'idle');
+ expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(
+ false
+ );
+ expect(lastRenderState(renderer).isPendingRefine).toBe(false);
- widget.render({
- helper,
- instantSearchInstance,
- results: {
- hits: [],
- },
- });
+ simulateEvent(mapInstance, eventName);
- expect(mapInstance.setCenter).not.toHaveBeenCalled();
- expect(mapInstance.setZoom).not.toHaveBeenCalled();
+ expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(
+ false
+ );
+ expect(lastRenderState(renderer).isPendingRefine).toBe(false);
+ });
});
});
@@ -1140,7 +925,9 @@ describe('GeoSearch', () => {
createOptions: item => ({
title: `ID: ${item.objectID}`,
}),
- template: '{{objectID}}
',
+ },
+ templates: {
+ HTMLMarker: '{{objectID}}
',
},
});
@@ -1190,6 +977,131 @@ describe('GeoSearch', () => {
createHTMLMarker.mockRestore();
});
+ it('expect to render custom HTML markers with only the template provided', () => {
+ const container = createContainer();
+ const instantSearchInstance = createFakeInstantSearch();
+ const helper = createFakeHelper();
+ const googleReference = createFakeGoogleReference();
+ const HTMLMarker = jest.fn(createFakeMarkerInstance);
+
+ createHTMLMarker.mockImplementation(() => HTMLMarker);
+
+ const widget = geoSearch({
+ googleReference,
+ container,
+ templates: {
+ HTMLMarker: '{{objectID}}
',
+ },
+ });
+
+ widget.init({
+ helper,
+ instantSearchInstance,
+ state: helper.state,
+ });
+
+ widget.render({
+ helper,
+ instantSearchInstance,
+ results: {
+ hits: [
+ { objectID: 123, _geoloc: true },
+ { objectID: 456, _geoloc: true },
+ { objectID: 789, _geoloc: true },
+ ],
+ },
+ });
+
+ expect(HTMLMarker).toHaveBeenCalledTimes(3);
+ expect(HTMLMarker.mock.calls).toEqual([
+ [
+ expect.objectContaining({
+ __id: 123,
+ template: '123
',
+ }),
+ ],
+ [
+ expect.objectContaining({
+ __id: 456,
+ template: '456
',
+ }),
+ ],
+ [
+ expect.objectContaining({
+ __id: 789,
+ template: '789
',
+ }),
+ ],
+ ]);
+
+ createHTMLMarker.mockRestore();
+ });
+
+ it('expect to render custom HTML markers with only the object provided', () => {
+ const container = createContainer();
+ const instantSearchInstance = createFakeInstantSearch();
+ const helper = createFakeHelper();
+ const googleReference = createFakeGoogleReference();
+ const HTMLMarker = jest.fn(createFakeMarkerInstance);
+
+ createHTMLMarker.mockImplementation(() => HTMLMarker);
+
+ const widget = geoSearch({
+ googleReference,
+ container,
+ customHTMLMarker: {
+ createOptions: item => ({
+ title: `ID: ${item.objectID}`,
+ }),
+ },
+ });
+
+ widget.init({
+ helper,
+ instantSearchInstance,
+ state: helper.state,
+ });
+
+ widget.render({
+ helper,
+ instantSearchInstance,
+ results: {
+ hits: [
+ { objectID: 123, _geoloc: true },
+ { objectID: 456, _geoloc: true },
+ { objectID: 789, _geoloc: true },
+ ],
+ },
+ });
+
+ expect(HTMLMarker).toHaveBeenCalledTimes(3);
+ expect(HTMLMarker.mock.calls).toEqual([
+ [
+ expect.objectContaining({
+ __id: 123,
+ title: 'ID: 123',
+ template: 'Your custom HTML Marker
',
+ }),
+ ],
+ [
+ expect.objectContaining({
+ __id: 456,
+ title: 'ID: 456',
+ template: 'Your custom HTML Marker
',
+ }),
+ ],
+ [
+ expect.objectContaining({
+ __id: 789,
+ title: 'ID: 789',
+ template: 'Your custom HTML Marker
',
+ }),
+ ],
+ ]);
+
+ createHTMLMarker.mockRestore();
+ });
+
it('expect to setup listeners on custom HTML markers', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
@@ -1441,8 +1353,8 @@ describe('GeoSearch', () => {
});
});
- describe('fit markers position', () => {
- it('expect to set the position', () => {
+ describe('update map position', () => {
+ it('expect to update the map position from the markers boundingBox', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
const helper = createFakeHelper();
@@ -1469,6 +1381,13 @@ describe('GeoSearch', () => {
});
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ }),
+ null
+ );
expect(renderer).toHaveBeenCalledTimes(2);
widget.render({
@@ -1483,37 +1402,51 @@ describe('GeoSearch', () => {
});
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(2);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ }),
+ null
+ );
expect(renderer).toHaveBeenCalledTimes(3);
});
- it("expect to set the position when it's refine with the map and the map is not render", () => {
+ it('expect to update the map position from the current refinement boundingBox', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
const helper = createFakeHelper();
const mapInstance = createFakeMapInstance();
const googleReference = createFakeGoogleReference({ mapInstance });
+ mapInstance.getBounds.mockImplementation(() => ({
+ getNorthEast: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 12,
+ lng: 14,
+ })),
+ })),
+ getSouthWest: jest.fn(() => ({
+ toJSON: jest.fn(() => ({
+ lat: 14,
+ lng: 16,
+ })),
+ })),
+ }));
+
const widget = geoSearch({
googleReference,
container,
});
- // Simulate external setter or URLSync
- helper.setQueryParameter('insideBoundingBox', [
- [
- 48.84174222399724,
- 2.367719162523599,
- 48.81614630305218,
- 2.284205902635904,
- ],
- ]);
-
widget.init({
helper,
instantSearchInstance,
state: helper.state,
});
+ simulateMapReadyEvent(googleReference);
+
widget.render({
helper,
instantSearchInstance,
@@ -1522,17 +1455,36 @@ describe('GeoSearch', () => {
},
});
- // Simulate map setter
- mapInstance.getZoom.mockImplementation(() => 12);
- mapInstance.getCenter.mockImplementation(() => ({
- lat: 10,
- lng: 12,
- }));
-
- expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(true);
+ expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(false);
+ expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(false);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ }),
+ null
+ );
expect(renderer).toHaveBeenCalledTimes(2);
+ // Simulate user interactions
+ simulateEvent(mapInstance, 'dragstart');
+ simulateEvent(mapInstance, 'center_changed');
+
+ expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(false);
+ expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 10, lng: 12 },
+ southWest: { lat: 12, lng: 14 },
+ }),
+ null
+ );
+ expect(renderer).toHaveBeenCalledTimes(3);
+
+ // Simulate refine
+ simulateEvent(mapInstance, 'idle');
+
widget.render({
helper,
instantSearchInstance,
@@ -1545,17 +1497,30 @@ describe('GeoSearch', () => {
});
expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(true);
- expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
- expect(renderer).toHaveBeenCalledTimes(3);
+ expect(mapInstance.fitBounds).toHaveBeenCalledTimes(2);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 12, lng: 14 },
+ southWest: { lat: 14, lng: 16 },
+ }),
+ 0
+ );
+ expect(renderer).toHaveBeenCalledTimes(4);
});
- it('expect to not set the position when there is no markers', () => {
+ it('expect to update the map position from the initial current refinement boundingBox', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
const helper = createFakeHelper();
const mapInstance = createFakeMapInstance();
const googleReference = createFakeGoogleReference({ mapInstance });
+ // Simulate the current refinement
+ helper.setQueryParameter(
+ 'insideBoundingBox',
+ '48.84174222399724, 2.367719162523599, 48.81614630305218, 2.284205902635904'
+ );
+
const widget = geoSearch({
googleReference,
container,
@@ -1575,22 +1540,41 @@ describe('GeoSearch', () => {
},
});
+ expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(true);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 48.84174222399724, lng: 2.367719162523599 },
+ southWest: { lat: 48.81614630305218, lng: 2.284205902635904 },
+ }),
+ 0
+ );
expect(renderer).toHaveBeenCalledTimes(2);
widget.render({
helper,
instantSearchInstance,
results: {
- hits: [],
+ hits: [
+ { objectID: 123, _geoloc: true },
+ { objectID: 456, _geoloc: true },
+ ],
},
});
- expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(true);
+ expect(mapInstance.fitBounds).toHaveBeenCalledTimes(2);
+ expect(mapInstance.fitBounds).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ northEast: { lat: 48.84174222399724, lng: 2.367719162523599 },
+ southWest: { lat: 48.81614630305218, lng: 2.284205902635904 },
+ }),
+ 0
+ );
expect(renderer).toHaveBeenCalledTimes(3);
});
- it('expect to not set the position when the map has move since last refine', () => {
+ it('expect to update the map position from the initial position & zoom without a boundingBox', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
const helper = createFakeHelper();
@@ -1608,8 +1592,6 @@ describe('GeoSearch', () => {
state: helper.state,
});
- simulateMapReadyEvent(googleReference);
-
widget.render({
helper,
instantSearchInstance,
@@ -1618,24 +1600,44 @@ describe('GeoSearch', () => {
},
});
- expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(false);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setZoom).toHaveBeenCalledTimes(0);
+ expect(mapInstance.setCenter).toHaveBeenCalledTimes(0);
+
expect(renderer).toHaveBeenCalledTimes(2);
- simulateEvent(mapInstance, 'center_changed');
+ widget.render({
+ helper,
+ instantSearchInstance,
+ results: {
+ hits: [],
+ },
+ });
- expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(true);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+
+ expect(mapInstance.setZoom).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setZoom).toHaveBeenLastCalledWith(1);
+
+ expect(mapInstance.setCenter).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setCenter).toHaveBeenLastCalledWith({
+ lat: 0,
+ lng: 0,
+ });
+
expect(renderer).toHaveBeenCalledTimes(3);
});
- it("expect to not set the position when it's refine with the map and the map is already render", () => {
+ it('expect to update the map position from the position & zoom without a boundingBox', () => {
const container = createContainer();
const instantSearchInstance = createFakeInstantSearch();
const helper = createFakeHelper();
const mapInstance = createFakeMapInstance();
const googleReference = createFakeGoogleReference({ mapInstance });
+ // Simulate the position
+ helper.setQueryParameter('aroundLatLng', '10, 12');
+
const widget = geoSearch({
googleReference,
container,
@@ -1647,8 +1649,6 @@ describe('GeoSearch', () => {
state: helper.state,
});
- simulateMapReadyEvent(googleReference);
-
widget.render({
helper,
instantSearchInstance,
@@ -1657,27 +1657,52 @@ describe('GeoSearch', () => {
},
});
- // Simulate map setter
- mapInstance.getZoom.mockImplementation(() => 12);
- mapInstance.getCenter.mockImplementation(() => ({
- lat: 10,
- lng: 12,
- }));
-
- expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(false);
- expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(false);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setZoom).toHaveBeenCalledTimes(0);
+ expect(mapInstance.setCenter).toHaveBeenCalledTimes(0);
expect(renderer).toHaveBeenCalledTimes(2);
- simulateEvent(mapInstance, 'dragstart');
- simulateEvent(mapInstance, 'center_changed');
+ widget.render({
+ helper,
+ instantSearchInstance,
+ results: {
+ hits: [],
+ },
+ });
- expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(true);
- expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(false);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+
+ expect(mapInstance.setZoom).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setZoom).toHaveBeenLastCalledWith(1);
+
+ expect(mapInstance.setCenter).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setCenter).toHaveBeenLastCalledWith({
+ lat: 10,
+ lng: 12,
+ });
+
expect(renderer).toHaveBeenCalledTimes(3);
+ });
- simulateEvent(mapInstance, 'idle');
+ it('expect to not update the map when it has moved since last refine', () => {
+ const container = createContainer();
+ const instantSearchInstance = createFakeInstantSearch();
+ const helper = createFakeHelper();
+ const mapInstance = createFakeMapInstance();
+ const googleReference = createFakeGoogleReference({ mapInstance });
+
+ const widget = geoSearch({
+ googleReference,
+ container,
+ });
+
+ widget.init({
+ helper,
+ instantSearchInstance,
+ state: helper.state,
+ });
+
+ simulateMapReadyEvent(googleReference);
widget.render({
helper,
@@ -1688,9 +1713,20 @@ describe('GeoSearch', () => {
});
expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(false);
- expect(lastRenderArgs(renderer).isRefinedWithMap()).toBe(true);
expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
- expect(renderer).toHaveBeenCalledTimes(4);
+ expect(mapInstance.setZoom).not.toHaveBeenCalled();
+ expect(mapInstance.setCenter).not.toHaveBeenCalled();
+ expect(renderer).toHaveBeenCalledTimes(2);
+
+ // Simulate user interaction
+ simulateEvent(mapInstance, 'dragstart');
+ simulateEvent(mapInstance, 'center_changed');
+
+ expect(lastRenderArgs(renderer).hasMapMoveSinceLastRefine()).toBe(true);
+ expect(mapInstance.fitBounds).toHaveBeenCalledTimes(1);
+ expect(mapInstance.setZoom).not.toHaveBeenCalled();
+ expect(mapInstance.setCenter).not.toHaveBeenCalled();
+ expect(renderer).toHaveBeenCalledTimes(3);
});
});
});
diff --git a/src/widgets/geo-search/defaultTemplates.js b/src/widgets/geo-search/defaultTemplates.js
index d3607088c3..73b9777c50 100644
--- a/src/widgets/geo-search/defaultTemplates.js
+++ b/src/widgets/geo-search/defaultTemplates.js
@@ -1,5 +1,6 @@
export default {
- clear: 'Clear the map refinement',
+ HTMLMarker: 'Your custom HTML Marker
',
+ reset: 'Clear the map refinement',
toggle: 'Search as I move the map',
redo: 'Redo search here',
};
diff --git a/src/widgets/geo-search/geo-search.js b/src/widgets/geo-search/geo-search.js
index b356193ead..41b5b7e2a3 100644
--- a/src/widgets/geo-search/geo-search.js
+++ b/src/widgets/geo-search/geo-search.js
@@ -1,13 +1,14 @@
import cx from 'classnames';
import noop from 'lodash/noop';
import { unmountComponentAtNode } from 'preact-compat';
-import { getContainerNode, bemHelper, renderTemplate } from '../../lib/utils';
+import { getContainerNode, renderTemplate } from '../../lib/utils';
+import { component } from '../../lib/suit';
import connectGeoSearch from '../../connectors/geo-search/connectGeoSearch';
import renderer from './GeoSearchRenderer';
import defaultTemplates from './defaultTemplates';
import createHTMLMarker from './createHTMLMarker';
-const bem = bemHelper('ais-geo-search');
+const suit = component('GeoSearch');
const usage = `Usage:
@@ -16,21 +17,17 @@ geoSearch({
googleReference,
[ initialZoom = 1 ],
[ initialPosition = { lat: 0, lng: 0 } ],
- [ paddingBoundingBox = { top: 0, right: 0, bottom: 0, right: 0 } ],
- [ cssClasses.{root,map,controls,clear,control,toggleLabel,toggleLabelActive,toggleInput,redo} = {} ],
- [ templates.{clear,toggle,redo} ],
+ [ cssClasses.{root,map,control,label,selectedLabel,input,redo,disabledRedo,reset} = {} ],
+ [ templates.{reset,toggle,redo} ],
[ mapOptions ],
[ builtInMarker ],
[ customHTMLMarker = false ],
+ [ enableRefine = true ],
[ enableClearMapRefinement = true ],
[ enableRefineControl = true ],
[ enableRefineOnMapMove = true ],
- [ enableGeolocationWithIP = true ],
- [ position ],
- [ radius ],
- [ precision ],
[ transformItems ],
-})
+});
Full documentation available at https://community.algolia.com/instantsearch.js/v2/widgets/geoSearch.html
`;
@@ -42,7 +39,6 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {object} CustomHTMLMarkerOptions
- * @property {string|function(item): string} template Template to use for the marker.
* @property {function(item): HTMLMarkerOptions} [createOptions] Function used to create the options passed to the HTMLMarker.
* @property {{ eventType: function(object) }} [events] Object that takes an event type (ex: `click`, `mouseover`) as key and a listener as value. The listener is provided with an object that contains `event`, `item`, `marker`, `map`.
*/
@@ -56,32 +52,25 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
/**
* @typedef {object} GeoSearchCSSClasses
- * @property {string|Array} [root] CSS class to add to the root element.
- * @property {string|Array} [map] CSS class to add to the map element.
- * @property {string|Array} [controls] CSS class to add to the controls element.
- * @property {string|Array} [clear] CSS class to add to the clear element.
- * @property {string|Array} [control] CSS class to add to the control element.
- * @property {string|Array} [toggleLabel] CSS class to add to the toggle label.
- * @property {string|Array} [toggleLabelActive] CSS class to add to toggle label when it's active.
- * @property {string|Array} [toggleInput] CSS class to add to the toggle input.
- * @property {string|Array} [redo] CSS class to add to the redo element.
+ * @property {string|Array} [root] The root div of the widget.
+ * @property {string|Array} [map] The map container of the widget.
+ * @property {string|Array} [control] The control element of the widget.
+ * @property {string|Array} [label] The label of the control element.
+ * @property {string|Array} [selectedLabel] The selected label of the control element.
+ * @property {string|Array} [input] The input of the control element.
+ * @property {string|Array} [redo] The redo search button.
+ * @property {string|Array} [disabledRedo] The disabled redo search button.
+ * @property {string|Array} [reset] The reset refinement button.
*/
/**
* @typedef {object} GeoSearchTemplates
- * @property {string|function(object): string} [clear] Template for the clear button.
+ * @property {string|function(object): string} [HTMLMarker] Template to use for the marker.
+ * @property {string|function(object): string} [reset] Template for the reset button.
* @property {string|function(object): string} [toggle] Template for the toggle label.
* @property {string|function(object): string} [redo] Template for the redo button.
*/
-/**
- * @typedef {object} Padding
- * @property {number} top The top padding in pixels.
- * @property {number} right The right padding in pixels.
- * @property {number} bottom The bottom padding in pixels.
- * @property {number} left The left padding in pixels.
- */
-
/**
* @typedef {object} LatLng
* @property {number} lat The latitude in degrees.
@@ -95,23 +84,16 @@ Full documentation available at https://community.algolia.com/instantsearch.js/v
* See [the documentation](https://developers.google.com/maps/documentation/javascript/tutorial) for more information.
* @property {number} [initialZoom=1] By default the map will set the zoom accordingly to the markers displayed on it. When we refine it may happen that the results are empty. For those situations we need to provide a zoom to render the map.
* @property {LatLng} [initialPosition={ lat: 0, lng: 0 }] By default the map will set the position accordingly to the markers displayed on it. When we refine it may happen that the results are empty. For those situations we need to provide a position to render the map. This option is ignored when the `position` is provided.
- * @property {Padding} [paddingBoundingBox={ top:0, right: 0, bottom:0, left: 0 }] Add an inner padding on the map when you refine.
* @property {GeoSearchTemplates} [templates] Templates to use for the widget.
* @property {GeoSearchCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {object} [mapOptions] Option forwarded to the Google Maps constructor.
* See [the documentation](https://developers.google.com/maps/documentation/javascript/reference/3/#MapOptions) for more information.
* @property {BuiltInMarkerOptions} [builtInMarker] Options for customize the built-in Google Maps marker. This option is ignored when the `customHTMLMarker` is provided.
- * @property {CustomHTMLMarkerOptions|boolean} [customHTMLMarker=false] Options for customize the HTML marker. We provide an alternative to the built-in Google Maps marker in order to have a full control of the marker rendering. You can use plain HTML to build your marker.
+ * @property {CustomHTMLMarkerOptions} [customHTMLMarker] Options for customize the HTML marker. We provide an alternative to the built-in Google Maps marker in order to have a full control of the marker rendering. You can use plain HTML to build your marker.
+ * @property {boolean} [enableRefine=true] If true, the map is used to search - otherwise it's for display purposes only.
* @property {boolean} [enableClearMapRefinement=true] If true, a button is displayed on the map when the refinement is coming from the map in order to remove it.
* @property {boolean} [enableRefineControl=true] If true, the user can toggle the option `enableRefineOnMapMove` directly from the map.
* @property {boolean} [enableRefineOnMapMove=true] If true, refine will be triggered as you move the map.
- * @property {boolean} [enableGeolocationWithIP=true] If true, the IP will be use for the geolocation. If the `position` option is provided this option will be ignored, since we already refine the results around the given position. See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP) for more information.
- * @property {LatLng} [position] Position that will be use to search around.
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng) for more information.
- * @property {number} [radius] Maximum radius to search around the position (in meters).
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius) for more information.
- * @property {number} [precision] Precision of geo search (in meters).
- * See [the documentation](https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision) for more information.
* @property {function} [transformItems] Function to transform the items passed to the templates.
*/
@@ -145,9 +127,9 @@ const geoSearch = ({
initialPosition = { lat: 0, lng: 0 },
templates: userTemplates = {},
cssClasses: userCssClasses = {},
- paddingBoundingBox: userPaddingBoundingBox = {},
builtInMarker: userBuiltInMarker = {},
- customHTMLMarker: userCustomHTMLMarker = false,
+ customHTMLMarker: userCustomHTMLMarker,
+ enableRefine = true,
enableClearMapRefinement = true,
enableRefineControl = true,
container,
@@ -160,18 +142,10 @@ const geoSearch = ({
};
const defaultCustomHTMLMarker = {
- template: 'Your custom HTML Marker
',
createOptions: noop,
events: {},
};
- const defaultPaddingBoundingBox = {
- top: 0,
- right: 0,
- bottom: 0,
- left: 0,
- };
-
if (!container) {
throw new Error(`Must provide a "container". ${usage}`);
}
@@ -183,18 +157,23 @@ const geoSearch = ({
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- map: cx(bem('map'), userCssClasses.map),
- controls: cx(bem('controls'), userCssClasses.controls),
- clear: cx(bem('clear'), userCssClasses.clear),
- control: cx(bem('control'), userCssClasses.control),
- toggleLabel: cx(bem('toggle-label'), userCssClasses.toggleLabel),
- toggleLabelActive: cx(
- bem('toggle-label-active'),
- userCssClasses.toggleLabelActive
+ root: cx(suit(), userCssClasses.root),
+ // Required only to mount / unmount the Preact tree
+ tree: suit({ descendantName: 'tree' }),
+ map: cx(suit({ descendantName: 'map' }), userCssClasses.map),
+ control: cx(suit({ descendantName: 'control' }), userCssClasses.control),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ selectedLabel: cx(
+ suit({ descendantName: 'label', modifierName: 'selected' }),
+ userCssClasses.selectedLabel
),
- toggleInput: cx(bem('toggle-input'), userCssClasses.toggleInput),
- redo: cx(bem('redo'), userCssClasses.redo),
+ input: cx(suit({ descendantName: 'input' }), userCssClasses.input),
+ redo: cx(suit({ descendantName: 'redo' }), userCssClasses.redo),
+ disabledRedo: cx(
+ suit({ descendantName: 'redo', modifierName: 'disabled' }),
+ userCssClasses.disabledRedo
+ ),
+ reset: cx(suit({ descendantName: 'reset' }), userCssClasses.reset),
};
const templates = {
@@ -207,16 +186,14 @@ const geoSearch = ({
...userBuiltInMarker,
};
- const customHTMLMarker = Boolean(userCustomHTMLMarker) && {
+ const isCustomHTMLMarker =
+ Boolean(userCustomHTMLMarker) || Boolean(userTemplates.HTMLMarker);
+
+ const customHTMLMarker = isCustomHTMLMarker && {
...defaultCustomHTMLMarker,
...userCustomHTMLMarker,
};
- const paddingBoundingBox = {
- ...defaultPaddingBoundingBox,
- ...userPaddingBoundingBox,
- };
-
const createBuiltInMarker = ({ item, ...rest }) =>
new googleReference.maps.Marker({
...builtInMarker.createOptions(item),
@@ -232,10 +209,10 @@ const geoSearch = ({
...rest,
__id: item.objectID,
position: item._geoloc,
- className: cx(bem('marker')),
+ className: cx(suit({ descendantName: 'marker' })),
template: renderTemplate({
- templateKey: 'template',
- templates: customHTMLMarker,
+ templateKey: 'HTMLMarker',
+ templates,
data: item,
}),
});
@@ -252,7 +229,7 @@ const geoSearch = ({
try {
const makeGeoSearch = connectGeoSearch(renderer, () => {
unmountComponentAtNode(
- containerNode.querySelector(`.${cssClasses.controls}`)
+ containerNode.querySelector(`.${cssClasses.tree}`)
);
while (containerNode.firstChild) {
@@ -269,9 +246,9 @@ const geoSearch = ({
initialPosition,
templates,
cssClasses,
- paddingBoundingBox,
createMarker,
markerOptions,
+ enableRefine,
enableClearMapRefinement,
enableRefineControl,
});
diff --git a/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap b/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
index 84995fe448..622dcbd5ba 100644
--- a/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
+++ b/src/widgets/hierarchical-menu/__tests__/__snapshots__/hierarchical-menu-test.js.snap
@@ -1,23 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`hierarchicalMenu() render calls ReactDOM.render 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
/>
`;
exports[`hierarchicalMenu() render has a templates option 1`] = `
-
`;
exports[`hierarchicalMenu() render has a transformItems options 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
/>
`;
exports[`hierarchicalMenu() render sets facetValues to empty array when no results 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`hierarchicalMenu() render sets shouldAutoHideContainer to true when no results 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
/>
`;
exports[`hierarchicalMenu() render understand provided cssClasses 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
toggleRefinement={[Function]}
+ toggleShowMore={[Function]}
/>
`;
diff --git a/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js b/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js
index 709a1036e9..64adeebb4d 100644
--- a/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js
+++ b/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.js
@@ -1,3 +1,4 @@
+import { SearchParameters } from 'algoliasearch-helper';
import hierarchicalMenu from '../hierarchical-menu';
describe('hierarchicalMenu()', () => {
@@ -143,24 +144,28 @@ describe('hierarchicalMenu()', () => {
toggleRefinement: jest.fn().mockReturnThis(),
search: jest.fn(),
};
- state = {
- toggleRefinement: jest.fn(),
- };
+ state = new SearchParameters();
+ state.toggleRefinement = jest.fn();
options = { container, attributes };
createURL = () => '#';
});
it('understand provided cssClasses', () => {
const userCssClasses = {
- root: ['root', 'cx'],
- header: 'header',
- body: 'body',
- footer: 'footer',
+ root: 'root',
+ noRefinementRoot: 'noRefinementRoot',
+ searchBox: 'searchBox',
list: 'list',
+ childList: 'childList',
item: 'item',
- active: 'active',
+ selectedItem: 'selectedItem',
+ parentItem: 'parentItem',
link: 'link',
+ label: 'label',
count: 'count',
+ noResults: 'noResults',
+ showMore: 'showMore',
+ disabledShowMore: 'disabledShowMore',
};
widget = hierarchicalMenu({ ...options, cssClasses: userCssClasses });
@@ -201,9 +206,7 @@ describe('hierarchicalMenu()', () => {
widget = hierarchicalMenu({
...options,
templates: {
- header: 'header2',
item: 'item2',
- footer: 'footer2',
},
});
widget.init({ helper, createURL, instantSearchInstance: {} });
@@ -224,14 +227,6 @@ describe('hierarchicalMenu()', () => {
expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
- it('sets shouldAutoHideContainer to true when no results', () => {
- data = {};
- widget = hierarchicalMenu(options);
- widget.init({ helper, createURL, instantSearchInstance: {} });
- widget.render({ results, state });
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
it('sets facetValues to empty array when no results', () => {
data = {};
widget = hierarchicalMenu(options);
@@ -289,15 +284,9 @@ describe('hierarchicalMenu()', () => {
ReactDOM.render.mock.calls[0][0].props.facetValues;
expect(actualFacetValues).toEqual(expectedFacetValues);
});
-
- afterEach(() => {
- hierarchicalMenu.__ResetDependency__('defaultTemplates');
- });
});
afterEach(() => {
hierarchicalMenu.__ResetDependency__('render');
- hierarchicalMenu.__ResetDependency__('autoHideContainerHOC');
- hierarchicalMenu.__ResetDependency__('headerFooterHOC');
});
});
diff --git a/src/widgets/hierarchical-menu/defaultTemplates.js b/src/widgets/hierarchical-menu/defaultTemplates.js
index 64def3bcd6..d9078ca1c8 100644
--- a/src/widgets/hierarchical-menu/defaultTemplates.js
+++ b/src/widgets/hierarchical-menu/defaultTemplates.js
@@ -1,7 +1,15 @@
-/* eslint-disable max-len */
export default {
- header: '',
item:
- '{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ',
- footer: '',
+ '' +
+ '{{label}} ' +
+ '{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ' +
+ ' ',
+ showMoreText: `
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ `,
};
diff --git a/src/widgets/hierarchical-menu/hierarchical-menu.js b/src/widgets/hierarchical-menu/hierarchical-menu.js
index cad20eb166..cdc7056cec 100644
--- a/src/widgets/hierarchical-menu/hierarchical-menu.js
+++ b/src/widgets/hierarchical-menu/hierarchical-menu.js
@@ -1,33 +1,33 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
-import connectHierarchicalMenu from '../../connectors/hierarchical-menu/connectHierarchicalMenu';
import RefinementList from '../../components/RefinementList/RefinementList.js';
+import connectHierarchicalMenu from '../../connectors/hierarchical-menu/connectHierarchicalMenu';
import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-hierarchical-menu');
+const suit = component('HierarchicalMenu');
const renderer = ({
- autoHideContainer,
- collapsible,
cssClasses,
containerNode,
- transformData,
+ showMore,
templates,
renderState,
}) => (
- { createURL, items, refine, instantSearchInstance },
+ {
+ createURL,
+ items,
+ refine,
+ instantSearchInstance,
+ isShowingMore,
+ toggleShowMore,
+ canToggleShowMore,
+ },
isFirstRendering
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -35,17 +35,17 @@ const renderer = ({
return;
}
- const shouldAutoHideContainer = autoHideContainer && items.length === 0;
-
render(
,
containerNode
);
@@ -55,52 +55,50 @@ const usage = `Usage:
hierarchicalMenu({
container,
attributes,
- [ separator=' > ' ],
+ [ separator = ' > ' ],
[ rootPath ],
- [ showParentLevel=false ],
- [ limit=10 ],
- [ sortBy=['name:asc'] ],
- [ cssClasses.{root , header, body, footer, list, depth, item, active, link}={} ],
- [ templates.{header, item, footer} ],
- [ transformData.{item} ],
- [ autoHideContainer=true ],
- [ collapsible=false ],
+ [ showParentLevel = true ],
+ [ limit = 10 ],
+ [ showMore = false ],
+ [ showMoreLimit = 20 ],
+ [ sortBy = ['name:asc'] ],
+ [ cssClasses.{root, noRefinementRoot, list, childList, item, selectedItem, parentItem, link, label, count, showMore, disabledShowMore} ],
+ [ templates.{item, showMoreText} ],
[ transformItems ]
})`;
+
/**
* @typedef {Object} HierarchicalMenuCSSClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element when no refinements.
* @property {string|string[]} [list] CSS class to add to the list element.
+ * @property {string|string[]} [childList] CSS class to add to the child list element.
* @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [depth] CSS class to add to each item element to denote its depth. The actual level will be appended to the given class name (ie. if `depth` is given, the widget will add `depth0`, `depth1`, ... according to the level of each item).
- * @property {string|string[]} [active] CSS class to add to each active element.
+ * @property {string|string[]} [selectedItem] CSS class to add to each selected item element.
+ * @property {string|string[]} [parentItem] CSS class to add to each parent item element.
* @property {string|string[]} [link] CSS class to add to each link (when using the default template).
+ * @property {string|string[]} [label] CSS class to add to each label (when using the default template).
* @property {string|string[]} [count] CSS class to add to each count element (when using the default template).
+ * @property {string|string[]} [showMore] CSS class to add to the show more element.
+ * @property {string|string[]} [disabledShowMore] CSS class to add to the disabled show more element.
*/
/**
* @typedef {Object} HierarchicalMenuTemplates
- * @property {string|function(object):string} [header=''] Header template (root level only).
* @property {string|function(object):string} [item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties.
- * @property {string|function(object):string} [footer=''] Footer template (root level only).
- */
-
-/**
- * @typedef {Object} HierarchicalMenuTransforms
- * @property {function(object):object} [item] Method to change the object passed to the `item`. template
+ * @property {string|function} [showMoreText] Template used for the show more text, provided with `isShowingMore` data property.
*/
/**
* @typedef {Object} HierarchicalMenuWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
* @property {string[]} attributes Array of attributes to use to generate the hierarchy of the menu.
- * @property {number} [limit=10] How much facet values to get.
- * @property {string} [separator=" > "] Separator used in the attributes to separate level values.
+ * @property {string} [separator = " > "] Separator used in the attributes to separate level values.
* @property {string} [rootPath] Prefix path to use if the first level is not the root level.
- * @property {boolean} [showParentLevel=true] Show the siblings of the selected parent level of the current refined value. This
+ * @property {boolean} [showParentLevel = true] Show the siblings of the selected parent level of the current refined value. This
+ * @property {number} [limit = 10] Max number of values to display.
+ * @property {boolean} [showMore = false] Whether to display the "show more" button.
+ * @property {number} [showMoreLimit = 20] Max number of values to display when showing more.
* does not impact the root level.
*
* The hierarchical menu is able to show or hide the siblings with `showParentLevel`.
@@ -122,17 +120,12 @@ hierarchicalMenu({
* - **lvl2**
* - Parent lvl0
* - Parent lvl0
- * @property {string[]|function} [sortBy=['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
+ * @property {string[]|function} [sortBy = ['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
* @property {HierarchicalMenuTemplates} [templates] Templates to use for the widget.
- * @property {HierarchicalMenuTransforms} [transformData] Set of functions to transform the data passed to the templates.
- * @property {boolean} [autoHideContainer=true] Hide the container when there are no items in the menu.
* @property {HierarchicalMenuCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
- * @property {boolean|{collapsed: boolean}} [collapsible=false] Makes the widget collapsible. The user can then
- * choose to hide the content of the widget. This option can also be an object with the property collapsed. If this
- * property is `true`, then the widget is hidden during the first rendering.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
/**
@@ -183,26 +176,22 @@ hierarchicalMenu({
* instantsearch.widgets.hierarchicalMenu({
* container: '#hierarchical-categories',
* attributes: ['hierarchicalCategories.lvl0', 'hierarchicalCategories.lvl1', 'hierarchicalCategories.lvl2'],
- * templates: {
- * header: 'Hierarchical categories'
- * }
* })
* );
*/
export default function hierarchicalMenu({
container,
attributes,
- separator = ' > ',
- rootPath = null,
- showParentLevel = true,
- limit = 10,
- sortBy = ['name:asc'],
- cssClasses: userCssClasses = {},
- autoHideContainer = true,
- templates = defaultTemplates,
- collapsible = false,
- transformData,
+ separator,
+ rootPath,
+ showParentLevel,
+ limit,
+ showMore = false,
+ showMoreLimit,
+ sortBy,
transformItems,
+ templates = defaultTemplates,
+ cssClasses: userCssClasses = {},
} = {}) {
if (!container || !attributes || !attributes.length) {
throw new Error(usage);
@@ -211,25 +200,40 @@ export default function hierarchicalMenu({
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- depth: bem('list', 'lvl'),
- item: cx(bem('item'), userCssClasses.item),
- active: cx(bem('item', 'active'), userCssClasses.active),
- link: cx(bem('link'), userCssClasses.link),
- count: cx(bem('count'), userCssClasses.count),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ childList: cx(
+ suit({ descendantName: 'list', modifierName: 'child' }),
+ userCssClasses.childList
+ ),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ parentItem: cx(
+ suit({ descendantName: 'item', modifierName: 'parent' }),
+ userCssClasses.parentItem
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ count: cx(suit({ descendantName: 'count' }), userCssClasses.count),
+ showMore: cx(suit({ descendantName: 'showMore' }), userCssClasses.showMore),
+ disabledShowMore: cx(
+ suit({ descendantName: 'showMore', modifierName: 'disabled' }),
+ userCssClasses.disabledShowMore
+ ),
};
const specializedRenderer = renderer({
- autoHideContainer,
- collapsible,
cssClasses,
containerNode,
- transformData,
templates,
+ showMore,
renderState: {},
});
@@ -238,16 +242,19 @@ export default function hierarchicalMenu({
specializedRenderer,
() => unmountComponentAtNode(containerNode)
);
+
return makeHierarchicalMenu({
attributes,
separator,
rootPath,
showParentLevel,
limit,
+ showMore,
+ showMoreLimit,
sortBy,
transformItems,
});
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap b/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap
deleted file mode 100644
index 55d5cb941a..0000000000
--- a/src/widgets/hits-per-page-selector/__tests__/__snapshots__/hits-per-page-selector-test.js.snap
+++ /dev/null
@@ -1,62 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`hitsPerPageSelector() calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`hitsPerPageSelector() renders transformed items 1`] = `
-
-`;
diff --git a/src/widgets/hits-per-page-selector/hits-per-page-selector.js b/src/widgets/hits-per-page-selector/hits-per-page-selector.js
deleted file mode 100644
index 85cbe5e794..0000000000
--- a/src/widgets/hits-per-page-selector/hits-per-page-selector.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import find from 'lodash/find';
-
-import Selector from '../../components/Selector.js';
-import connectHitsPerPage from '../../connectors/hits-per-page/connectHitsPerPage.js';
-
-import { bemHelper, getContainerNode } from '../../lib/utils.js';
-
-const bem = bemHelper('ais-hits-per-page-selector');
-
-const renderer = ({ containerNode, cssClasses, autoHideContainer }) => (
- { items, refine, hasNoResults },
- isFirstRendering
-) => {
- if (isFirstRendering) return;
-
- const { value: currentValue } =
- find(items, ({ isRefined }) => isRefined) || {};
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-hitsPerPageSelector({
- container,
- items,
- [ cssClasses.{root,select,item}={} ],
- [ autoHideContainer=false ],
- [ transformItems ]
-})`;
-
-/**
- * @typedef {Object} HitsPerPageSelectorCSSClasses
- * @property {string|string[]} [root] CSS classes added to the outer ``.
- * @property {string|string[]} [select] CSS classes added to the parent `
`.
- * @property {string|string[]} [item] CSS classes added to each ``.
- */
-
-/**
- * @typedef {Object} HitsPerPageSelectorItems
- * @property {number} value number of hits to display per page.
- * @property {string} label Label to display in the option.
- * @property {boolean} default The default hits per page on first search.
- */
-
-/**
- * @typedef {Object} HitsPerPageSelectorWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {HitsPerPageSelectorItems[]} items Array of objects defining the different values and labels.
- * @property {boolean} [autoHideContainer=false] Hide the container when no results match.
- * @property {HitsPerPageSelectorCSSClasses} [cssClasses] CSS classes to be added.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * The hitsPerPageSelector widget gives the user the ability to change the number of results
- * displayed in the hits widget.
- *
- * You can specify the default hits per page using a boolean in the items[] array. If none is specified, this first hits per page option will be picked.
- * @type {WidgetFactory}
- * @devNovel HitsPerPageSelector
- * @category basic
- * @param {HitsPerPageSelectorWidgetOptions} $0 The options of the HitPerPageSelector widget.
- * @return {Widget} A new instance of the HitPerPageSelector widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.hitsPerPageSelector({
- * container: '#hits-per-page-selector',
- * items: [
- * {value: 3, label: '3 per page', default: true},
- * {value: 6, label: '6 per page'},
- * {value: 12, label: '12 per page'},
- * ]
- * })
- * );
- */
-export default function hitsPerPageSelector({
- container,
- items,
- cssClasses: userCssClasses = {},
- autoHideContainer = false,
- transformItems,
-} = {}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- // We use the same class to avoid regression on existing website. It needs to be replaced
- // eventually by `bem('select')
- select: cx(bem(null), userCssClasses.select),
- item: cx(bem('item'), userCssClasses.item),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- cssClasses,
- autoHideContainer,
- });
-
- try {
- const makeHitsPerPageSelector = connectHitsPerPage(
- specializedRenderer,
- () => unmountComponentAtNode(containerNode)
- );
- return makeHitsPerPageSelector({ items, transformItems });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/hits-per-page/__tests__/__snapshots__/hits-per-page-test.js.snap b/src/widgets/hits-per-page/__tests__/__snapshots__/hits-per-page-test.js.snap
new file mode 100644
index 0000000000..68c10631e3
--- /dev/null
+++ b/src/widgets/hits-per-page/__tests__/__snapshots__/hits-per-page-test.js.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`hitsPerPage() calls twice ReactDOM.render( , container) 1`] = `
+
+
+
+`;
+
+exports[`hitsPerPage() renders transformed items 1`] = `
+
+
+
+`;
diff --git a/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js b/src/widgets/hits-per-page/__tests__/hits-per-page-test.js
similarity index 78%
rename from src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js
rename to src/widgets/hits-per-page/__tests__/hits-per-page-test.js
index 0c09956fa2..fe20deb566 100644
--- a/src/widgets/hits-per-page-selector/__tests__/hits-per-page-selector-test.js
+++ b/src/widgets/hits-per-page/__tests__/hits-per-page-test.js
@@ -1,18 +1,18 @@
-import hitsPerPageSelector from '../hits-per-page-selector';
+import hitsPerPage from '../hits-per-page';
-describe('hitsPerPageSelector call', () => {
+describe('hitsPerPage call', () => {
it('throws an exception when no items', () => {
const container = document.createElement('div');
- expect(hitsPerPageSelector.bind(null, { container })).toThrow(/^Usage:/);
+ expect(hitsPerPage.bind(null, { container })).toThrow(/^Usage:/);
});
it('throws an exception when no container', () => {
const items = { a: { value: 'value', label: 'My value' } };
- expect(hitsPerPageSelector.bind(null, { items })).toThrow(/^Usage:/);
+ expect(hitsPerPage.bind(null, { items })).toThrow(/^Usage:/);
});
});
-describe('hitsPerPageSelector()', () => {
+describe('hitsPerPage()', () => {
let ReactDOM;
let container;
let items;
@@ -26,7 +26,7 @@ describe('hitsPerPageSelector()', () => {
beforeEach(() => {
ReactDOM = { render: jest.fn() };
- hitsPerPageSelector.__Rewire__('render', ReactDOM.render);
+ hitsPerPage.__Rewire__('render', ReactDOM.render);
consoleWarn = jest.spyOn(window.console, 'warn');
container = document.createElement('div');
@@ -37,9 +37,9 @@ describe('hitsPerPageSelector()', () => {
cssClasses = {
root: ['custom-root', 'cx'],
select: 'custom-select',
- item: 'custom-item',
+ option: 'custom-option',
};
- widget = hitsPerPageSelector({ container, items, cssClasses });
+ widget = hitsPerPage({ container, items, cssClasses });
helper = {
state: {
hitsPerPage: 20,
@@ -62,7 +62,7 @@ describe('hitsPerPageSelector()', () => {
});
it('does configures the default hits per page if specified', () => {
- const widgetWithDefaults = hitsPerPageSelector({
+ const widgetWithDefaults = hitsPerPage({
container: document.createElement('div'),
items: [
{ value: 10, label: '10 results' },
@@ -84,7 +84,7 @@ describe('hitsPerPageSelector()', () => {
});
it('renders transformed items', () => {
- widget = hitsPerPageSelector({
+ widget = hitsPerPage({
container,
items: [
{ value: 10, label: '10 results' },
@@ -115,9 +115,8 @@ describe('hitsPerPageSelector()', () => {
items.push({ label: 'Label without a value' });
widget.init({ state: helper.state, helper });
expect(consoleWarn).toHaveBeenCalledTimes(1, 'console.warn called once');
- expect(consoleWarn.mock.calls[0][0]).toEqual(
- `[InstantSearch.js]: [hitsPerPageSelector] No item in \`items\`
- with \`value: hitsPerPage\` (hitsPerPage: 20)`
+ expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
+ `"[InstantSearch.js]: No items in HitsPerPage \`items\` with \`value: hitsPerPage\` (hitsPerPage: 20)"`
);
});
@@ -125,9 +124,8 @@ describe('hitsPerPageSelector()', () => {
helper.state.hitsPerPage = -1;
widget.init({ state: helper.state, helper });
expect(consoleWarn).toHaveBeenCalledTimes(1, 'console.warn called once');
- expect(consoleWarn.mock.calls[0][0]).toEqual(
- `[InstantSearch.js]: [hitsPerPageSelector] No item in \`items\`
- with \`value: hitsPerPage\` (hitsPerPage: -1)`
+ expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(
+ `"[InstantSearch.js]: No items in HitsPerPage \`items\` with \`value: hitsPerPage\` (hitsPerPage: -1)"`
);
});
@@ -139,7 +137,7 @@ describe('hitsPerPageSelector()', () => {
});
afterEach(() => {
- hitsPerPageSelector.__ResetDependency__('render');
+ hitsPerPage.__ResetDependency__('render');
consoleWarn.mockRestore();
});
});
diff --git a/src/widgets/hits-per-page/hits-per-page.js b/src/widgets/hits-per-page/hits-per-page.js
new file mode 100644
index 0000000000..692957025c
--- /dev/null
+++ b/src/widgets/hits-per-page/hits-per-page.js
@@ -0,0 +1,116 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import find from 'lodash/find';
+import Selector from '../../components/Selector/Selector.js';
+import connectHitsPerPage from '../../connectors/hits-per-page/connectHitsPerPage.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('HitsPerPage');
+
+const renderer = ({ containerNode, cssClasses }) => (
+ { items, refine },
+ isFirstRendering
+) => {
+ if (isFirstRendering) return;
+
+ const { value: currentValue } =
+ find(items, ({ isRefined }) => isRefined) || {};
+
+ render(
+
+
+
,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+hitsPerPage({
+ container,
+ items,
+ [ cssClasses.{root, select, option} ],
+ [ transformItems ]
+})`;
+
+/**
+ * @typedef {Object} HitsPerPageCSSClasses
+ * @property {string|string[]} [root] CSS classes added to the outer ``.
+ * @property {string|string[]} [select] CSS classes added to the parent `
`.
+ * @property {string|string[]} [option] CSS classes added to each ``.
+ */
+
+/**
+ * @typedef {Object} HitsPerPageItems
+ * @property {number} value number of hits to display per page.
+ * @property {string} label Label to display in the option.
+ * @property {boolean} default The default hits per page on first search.
+ */
+
+/**
+ * @typedef {Object} HitsPerPageWidgetOptions
+ * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
+ * @property {HitsPerPageItems[]} items Array of objects defining the different values and labels.
+ * @property {HitsPerPageCSSClasses} [cssClasses] CSS classes to be added.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * The hitsPerPage widget gives the user the ability to change the number of results
+ * displayed in the hits widget.
+ *
+ * You can specify the default hits per page using a boolean in the items[] array. If none is specified, this first hits per page option will be picked.
+ * @type {WidgetFactory}
+ * @devNovel HitsPerPage
+ * @category basic
+ * @param {HitsPerPageWidgetOptions} $0 The options of the HitPerPageSelector widget.
+ * @return {Widget} A new instance of the HitPerPageSelector widget.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.hitsPerPage({
+ * container: '#hits-per-page',
+ * items: [
+ * {value: 3, label: '3 per page', default: true},
+ * {value: 6, label: '6 per page'},
+ * {value: 12, label: '12 per page'},
+ * ]
+ * })
+ * );
+ */
+export default function hitsPerPage({
+ container,
+ items,
+ cssClasses: userCssClasses = {},
+ transformItems,
+} = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ select: cx(suit({ descendantName: 'select' }), userCssClasses.select),
+ option: cx(suit({ descendantName: 'option' }), userCssClasses.option),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ });
+
+ try {
+ const makeHitsPerPage = connectHitsPerPage(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeHitsPerPage({ items, transformItems });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap b/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
index 36929b4314..e18f2497f5 100644
--- a/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
+++ b/src/widgets/hits/__tests__/__snapshots__/hits-test.js.snap
@@ -4,9 +4,10 @@ exports[`hits() calls twice ReactDOM.render( , container) 1`] = `
, container) 1`] = `
"item": [Function],
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
"empty": false,
"item": false,
@@ -48,9 +48,10 @@ exports[`hits() calls twice ReactDOM.render( , container) 2`] = `
, container) 2`] = `
"item": [Function],
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
"empty": false,
"item": false,
@@ -92,9 +92,10 @@ exports[`hits() renders transformed items 1`] = `
{
container = document.createElement('div');
templateProps = {
- transformData: undefined,
templatesConfig: undefined,
templates: defaultTemplates,
useCustomCompileOptions: { item: false, empty: false },
diff --git a/src/widgets/hits/hits.js b/src/widgets/hits/hits.js
index e994b7593e..2405ddb484 100644
--- a/src/widgets/hits/hits.js
+++ b/src/widgets/hits/hits.js
@@ -1,31 +1,19 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
-import Hits from '../../components/Hits.js';
import connectHits from '../../connectors/hits/connectHits.js';
+import Hits from '../../components/Hits/Hits.js';
import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-hits');
+const suit = component('Hits');
-const renderer = ({
- renderState,
- cssClasses,
- containerNode,
- transformData,
- templates,
-}) => (
+const renderer = ({ renderState, cssClasses, containerNode, templates }) => (
{ hits: receivedHits, results, instantSearchInstance },
isFirstRendering
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -48,15 +36,15 @@ const usage = `Usage:
hits({
container,
[ transformItems ],
- [ cssClasses.{root,empty,item}={} ],
- [ templates.{empty,item} | templates.{empty, allItems} ],
- [ transformData.{empty,item} | transformData.{empty, allItems} ],
+ [ cssClasses.{root, emptyRoot, item} ],
+ [ templates.{empty, item} ],
})`;
/**
* @typedef {Object} HitsCSSClasses
* @property {string|string[]} [root] CSS class to add to the wrapping element.
- * @property {string|string[]} [empty] CSS class to add to the wrapping element when no results.
+ * @property {string|string[]} [emptyRoot] CSS class to add to the wrapping element when no results.
+ * @property {string|string[]} [list] CSS class to add to the list of results.
* @property {string|string[]} [item] CSS class to add to each result.
*/
@@ -64,23 +52,14 @@ hits({
* @typedef {Object} HitsTemplates
* @property {string|function(object):string} [empty=''] Template to use when there are no results.
* @property {string|function(object):string} [item=''] Template to use for each result. This template will receive an object containing a single record. The record will have a new property `__hitIndex` for the position of the record in the list of displayed hits.
- * @property {string|function(object):string} [allItems=''] Template to use for the list of all results. (Can't be used with `item` template). This template will receive a complete SearchResults result object, this object contains the key hits that contains all the records retrieved.
- */
-
-/**
- * @typedef {Object} HitsTransforms
- * @property {function(object):object} [empty] Method used to change the object passed to the `empty` template.
- * @property {function(object):object} [item] Method used to change the object passed to the `item` template.
- * @property {function(object):object} [allItems] Method used to change the object passed to the `allItems` template.
*/
/**
* @typedef {Object} HitsWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
* @property {HitsTemplates} [templates] Templates to use for the widget.
- * @property {HitsTransforms} [transformData] Method to change the object passed to the templates.
* @property {HitsCSSClasses} [cssClasses] CSS classes to add.
- * @property {boolean} [escapeHits = false] Escape HTML entities from hits string values.
+ * @property {boolean} [escapeHTML = true] Escape HTML entities from hits string values.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -103,18 +82,16 @@ hits({
* empty: 'No results',
* item: 'Hit {{objectID}} : {{{_highlightResult.name.value}}}'
* },
- * escapeHits: true,
* transformItems: items => items.map(item => item),
* })
* );
*/
export default function hits({
container,
- cssClasses: userCssClasses = {},
- templates = defaultTemplates,
- transformData,
- escapeHits = false,
+ escapeHTML,
transformItems,
+ templates = defaultTemplates,
+ cssClasses: userCssClasses = {},
}) {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
@@ -126,16 +103,16 @@ export default function hits({
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- item: cx(bem('item'), userCssClasses.item),
- empty: cx(bem(null, 'empty'), userCssClasses.empty),
+ root: cx(suit(), userCssClasses.root),
+ emptyRoot: cx(suit({ modifierName: 'empty' }), userCssClasses.emptyRoot),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
renderState: {},
- transformData,
templates,
});
@@ -143,8 +120,8 @@ export default function hits({
const makeHits = connectHits(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeHits({ escapeHits, transformItems });
- } catch (e) {
+ return makeHits({ escapeHTML, transformItems });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/index.js b/src/widgets/index.js
index 88bf6b9c39..49a42dda48 100644
--- a/src/widgets/index.js
+++ b/src/widgets/index.js
@@ -5,47 +5,37 @@
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
*/
-export { default as clearAll } from '../widgets/clear-all/clear-all.js';
-export { default as configure } from '../widgets/configure/configure.js';
export {
- default as currentRefinedValues,
-} from '../widgets/current-refined-values/current-refined-values.js';
-export { default as geoSearch } from '../widgets/geo-search/geo-search.js';
+ default as clearRefinements,
+} from './clear-refinements/clear-refinements';
+export { default as configure } from './configure/configure.js';
export {
- default as hierarchicalMenu,
-} from '../widgets/hierarchical-menu/hierarchical-menu.js';
-export { default as hits } from '../widgets/hits/hits.js';
-export {
- default as hitsPerPageSelector,
-} from '../widgets/hits-per-page-selector/hits-per-page-selector.js';
+ default as currentRefinements,
+} from './current-refinements/current-refinements.js';
+export { default as geoSearch } from './geo-search/geo-search.js';
export {
- default as infiniteHits,
-} from '../widgets/infinite-hits/infinite-hits.js';
-export { default as menu } from '../widgets/menu/menu.js';
+ default as hierarchicalMenu,
+} from './hierarchical-menu/hierarchical-menu.js';
+export { default as hits } from './hits/hits.js';
+export { default as hitsPerPage } from './hits-per-page/hits-per-page.js';
+export { default as infiniteHits } from './infinite-hits/infinite-hits.js';
+export { default as menu } from './menu/menu.js';
export {
default as refinementList,
-} from '../widgets/refinement-list/refinement-list.js';
-export {
- default as numericRefinementList,
-} from '../widgets/numeric-refinement-list/numeric-refinement-list.js';
-export {
- default as numericSelector,
-} from '../widgets/numeric-selector/numeric-selector.js';
-export { default as pagination } from '../widgets/pagination/pagination.js';
-export {
- default as priceRanges,
-} from '../widgets/price-ranges/price-ranges.js';
-export { default as rangeInput } from '../widgets/range-input/range-input.js';
-export { default as searchBox } from '../widgets/search-box/search-box.js';
-export {
- default as rangeSlider,
-} from '../widgets/range-slider/range-slider.js';
-export {
- default as sortBySelector,
-} from '../widgets/sort-by-selector/sort-by-selector.js';
-export { default as starRating } from '../widgets/star-rating/star-rating.js';
-export { default as stats } from '../widgets/stats/stats.js';
-export { default as toggle } from '../widgets/toggle/toggle.js';
-export { default as analytics } from '../widgets/analytics/analytics.js';
-export { default as breadcrumb } from '../widgets/breadcrumb/breadcrumb.js';
-export { default as menuSelect } from '../widgets/menu-select/menu-select.js';
+} from './refinement-list/refinement-list.js';
+export { default as numericMenu } from './numeric-menu/numeric-menu';
+export { default as pagination } from './pagination/pagination.js';
+export { default as rangeInput } from './range-input/range-input.js';
+export { default as searchBox } from './search-box/search-box.js';
+export { default as rangeSlider } from './range-slider/range-slider.js';
+export { default as sortBy } from './sort-by/sort-by.js';
+export { default as ratingMenu } from './rating-menu/rating-menu';
+export { default as stats } from './stats/stats.js';
+export {
+ default as toggleRefinement,
+} from './toggleRefinement/toggleRefinement.js';
+export { default as analytics } from './analytics/analytics.js';
+export { default as breadcrumb } from './breadcrumb/breadcrumb.js';
+export { default as menuSelect } from './menu-select/menu-select.js';
+export { default as poweredBy } from './powered-by/powered-by.js';
+export { default as panel } from './panel/panel.js';
diff --git a/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap b/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
index 9d3fb13117..60326c20e2 100644
--- a/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
+++ b/src/widgets/infinite-hits/__tests__/__snapshots__/infinite-hits-test.js.snap
@@ -4,11 +4,12 @@ exports[`infiniteHits() calls twice ReactDOM.render( , container) 1
, container) 1
}
}
showMore={[Function]}
- showMoreLabel="Show more results"
templateProps={
Object {
"templates": Object {
"empty": "No results",
"item": [Function],
+ "showMoreText": "Show more results",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
"empty": false,
"item": false,
+ "showMoreText": false,
},
}
}
@@ -53,11 +54,12 @@ exports[`infiniteHits() calls twice ReactDOM.render( , container) 2
, container) 2
}
}
showMore={[Function]}
- showMoreLabel="Show more results"
templateProps={
Object {
"templates": Object {
"empty": "No results",
"item": [Function],
+ "showMoreText": "Show more results",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
"empty": false,
"item": false,
+ "showMoreText": false,
},
}
}
@@ -102,11 +104,12 @@ exports[`infiniteHits() if it is the last page, then the props should contain is
{
@@ -24,7 +25,7 @@ describe('infiniteHits()', () => {
container = document.createElement('div');
widget = infiniteHits({
container,
- escapeHits: true,
+ escapeHTML: true,
cssClasses: { root: ['root', 'cx'] },
});
widget.init({ helper, instantSearchInstance: {} });
@@ -33,8 +34,8 @@ describe('infiniteHits()', () => {
it('It does have a specific configuration', () => {
expect(widget.getConfiguration()).toEqual({
- highlightPostTag: '__/ais-highlight__',
- highlightPreTag: '__ais-highlight__',
+ highlightPreTag: TAG_PLACEHOLDER.highlightPreTag,
+ highlightPostTag: TAG_PLACEHOLDER.highlightPostTag,
});
});
@@ -117,6 +118,5 @@ describe('infiniteHits()', () => {
afterEach(() => {
infiniteHits.__ResetDependency__('render');
- infiniteHits.__ResetDependency__('defaultTemplates');
});
});
diff --git a/src/widgets/infinite-hits/defaultTemplates.js b/src/widgets/infinite-hits/defaultTemplates.js
index 41953b4d43..220849dbf0 100644
--- a/src/widgets/infinite-hits/defaultTemplates.js
+++ b/src/widgets/infinite-hits/defaultTemplates.js
@@ -1,5 +1,6 @@
export default {
empty: 'No results',
+ showMoreText: 'Show more results',
item(data) {
return JSON.stringify(data, null, 2);
},
diff --git a/src/widgets/infinite-hits/infinite-hits.js b/src/widgets/infinite-hits/infinite-hits.js
index 01fe1dd2c7..3379f04ada 100644
--- a/src/widgets/infinite-hits/infinite-hits.js
+++ b/src/widgets/infinite-hits/infinite-hits.js
@@ -1,32 +1,19 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
-import InfiniteHits from '../../components/InfiniteHits.js';
+import InfiniteHits from '../../components/InfiniteHits/InfiniteHits.js';
import defaultTemplates from './defaultTemplates.js';
import connectInfiniteHits from '../../connectors/infinite-hits/connectInfiniteHits.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-infinite-hits');
+const suit = component('InfiniteHits');
-const renderer = ({
- cssClasses,
- containerNode,
- renderState,
- templates,
- transformData,
- showMoreLabel,
-}) => (
+const renderer = ({ cssClasses, containerNode, renderState, templates }) => (
{ hits, results, showMore, isLastPage, instantSearchInstance },
isFirstRendering
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -40,7 +27,6 @@ const renderer = ({
hits={hits}
results={results}
showMore={showMore}
- showMoreLabel={showMoreLabel}
templateProps={renderState.templateProps}
isLastPage={isLastPage}
/>,
@@ -52,43 +38,35 @@ const usage = `
Usage:
infiniteHits({
container,
- [ escapeHits = false ],
+ [ escapeHTML = true ],
[ transformItems ],
- [ showMoreLabel ],
- [ cssClasses.{root,empty,item,showmore,showmoreButton}={} ],
- [ templates.{empty,item} | templates.{empty} ],
- [ transformData.{empty,item} | transformData.{empty} ],
+ [ cssClasses.{root, emptyRoot, list, item, loadMore, disabledLoadMore} ],
+ [ templates.{empty, item, showMoreText} ],
})`;
/**
* @typedef {Object} InfiniteHitsTemplates
- * @property {string|function} [empty=""] Template used when there are no results.
- * @property {string|function} [item=""] Template used for each result. This template will receive an object containing a single record.
- */
-
-/**
- * @typedef {Object} InfiniteHitsTransforms
- * @property {function} [empty] Method used to change the object passed to the `empty` template.
- * @property {function} [item] Method used to change the object passed to the `item` template.
+ * @property {string|function} [empty = "No results"] Template used when there are no results.
+ * @property {string|function} [showMoreText = "Show more results"] Template used for the "load more" button.
+ * @property {string|function} [item = ""] Template used for each result. This template will receive an object containing a single record.
*/
/**
* @typedef {object} InfiniteHitsCSSClasses
* @property {string|string[]} [root] CSS class to add to the wrapping element.
- * @property {string|string[]} [empty] CSS class to add to the wrapping element when no results.
+ * @property {string|string[]} [emptyRoot] CSS class to add to the wrapping element when no results.
+ * @property {string|string[]} [list] CSS class to add to the list of results.
* @property {string|string[]} [item] CSS class to add to each result.
- * @property {string|string[]} [showmore] CSS class to add to the show more button container.
- * @property {string|string[]} [showmoreButton] CSS class to add to the show more button.
+ * @property {string|string[]} [loadMore] CSS class to add to the load more button.
+ * @property {string|string[]} [disabledLoadMore] CSS class to add to the load more button when disabled.
*/
/**
* @typedef {Object} InfiniteHitsWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
* @property {InfiniteHitsTemplates} [templates] Templates to use for the widget.
- * @property {string} [showMoreLabel="Show more results"] label used on the show more button.
- * @property {InfiniteHitsTransforms} [transformData] Method to change the object passed to the templates.
* @property {InfiniteHitsCSSClasses} [cssClasses] CSS classes to add.
- * @property {boolean} [escapeHits = false] Escape HTML entities from hits string values.
+ * @property {boolean} [escapeHTML = true] Escape HTML entities from hits string values.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -109,21 +87,19 @@ infiniteHits({
* container: '#infinite-hits-container',
* templates: {
* empty: 'No results',
+ * showMoreText: 'Show more results',
* item: 'Hit {{objectID}} : {{{_highlightResult.name.value}}}'
* },
- * escapeHits: true,
* transformItems: items => items.map(item => item),
* })
* );
*/
export default function infiniteHits({
container,
- cssClasses: userCssClasses = {},
- showMoreLabel = 'Show more results',
- templates = defaultTemplates,
- transformData,
- escapeHits = false,
+ escapeHTML,
transformItems,
+ templates = defaultTemplates,
+ cssClasses: userCssClasses = {},
} = {}) {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
@@ -139,19 +115,21 @@ export default function infiniteHits({
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- item: cx(bem('item'), userCssClasses.item),
- empty: cx(bem(null, 'empty'), userCssClasses.empty),
- showmore: cx(bem('showmore'), userCssClasses.showmore),
- showmoreButton: cx(bem('showmoreButton'), userCssClasses.showmoreButton),
+ root: cx(suit(), userCssClasses.root),
+ emptyRoot: cx(suit({ modifierName: 'empty' }), userCssClasses.emptyRoot),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ loadMore: cx(suit({ descendantName: 'loadMore' }), userCssClasses.loadMore),
+ disabledLoadMore: cx(
+ suit({ descendantName: 'loadMore', modifierName: 'disabled' }),
+ userCssClasses.disabledLoadMore
+ ),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
- transformData,
templates,
- showMoreLabel,
renderState: {},
});
@@ -159,8 +137,8 @@ export default function infiniteHits({
const makeInfiniteHits = connectInfiniteHits(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeInfiniteHits({ escapeHits, transformItems });
- } catch (e) {
+ return makeInfiniteHits({ escapeHTML, transformItems });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap b/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
index 9dc10401dc..0cf5754532 100644
--- a/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
+++ b/src/widgets/menu-select/__tests__/__snapshots__/menu-select-test.js.snap
@@ -1,15 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`menuSelect render renders correctly 1`] = `
- {
- it('throws an exception when no attributeName', () => {
+ it('throws an exception when no attribute', () => {
const container = document.createElement('div');
expect(menuSelect.bind(null, { container })).toThrow(/^Usage/);
});
it('throws an exception when no container', () => {
- const attributeName = 'categories';
- expect(menuSelect.bind(null, { attributeName })).toThrow(/^Usage/);
+ const attribute = 'categories';
+ expect(menuSelect.bind(null, { attribute })).toThrow(/^Usage/);
});
describe('render', () => {
@@ -35,7 +35,7 @@ describe('menuSelect', () => {
it('renders correctly', () => {
const widget = menuSelect({
container: document.createElement('div'),
- attributeName: 'test',
+ attribute: 'test',
});
widget.init({ helper, createURL: () => '#', instantSearchInstance: {} });
@@ -47,7 +47,7 @@ describe('menuSelect', () => {
it('renders transformed items correctly', () => {
const widget = menuSelect({
container: document.createElement('div'),
- attributeName: 'test',
+ attribute: 'test',
transformItems: items =>
items.map(item => ({ ...item, transformed: true })),
});
diff --git a/src/widgets/menu-select/defaultTemplates.js b/src/widgets/menu-select/defaultTemplates.js
index abb86d90c9..bbad8d4cc8 100644
--- a/src/widgets/menu-select/defaultTemplates.js
+++ b/src/widgets/menu-select/defaultTemplates.js
@@ -1,8 +1,5 @@
-/* eslint-disable max-len */
export default {
- header: '',
item:
'{{label}} ({{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}})',
- footer: '',
- seeAllOption: 'See all',
+ defaultOption: 'See all',
};
diff --git a/src/widgets/menu-select/menu-select.js b/src/widgets/menu-select/menu-select.js
index 3347320f83..44cfccb677 100644
--- a/src/widgets/menu-select/menu-select.js
+++ b/src/widgets/menu-select/menu-select.js
@@ -1,32 +1,19 @@
import React, { render } from 'preact-compat';
import cx from 'classnames';
-
import connectMenu from '../../connectors/menu/connectMenu';
+import MenuSelect from '../../components/MenuSelect/MenuSelect';
import defaultTemplates from './defaultTemplates';
-import MenuSelect from '../../components/MenuSelect';
-
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils';
+import { component } from '../../lib/suit';
-const bem = bemHelper('ais-menu-select');
+const suit = component('MenuSelect');
-const renderer = ({
- containerNode,
- cssClasses,
- autoHideContainer,
- renderState,
- templates,
- transformData,
-}) => (
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
{ refine, items, canRefine, instantSearchInstance },
isFirstRendering
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -34,15 +21,12 @@ const renderer = ({
return;
}
- const shouldAutoHideContainer = autoHideContainer && !canRefine;
-
render(
,
containerNode
@@ -52,49 +36,38 @@ const renderer = ({
const usage = `Usage:
menuSelect({
container,
- attributeName,
+ attribute,
[ sortBy=['name:asc'] ],
[ limit=10 ],
- [ cssClasses.{root,select,option,header,footer} ]
- [ templates.{header,item,footer,seeAllOption} ],
- [ transformData.{item} ],
- [ autoHideContainer ]
+ [ cssClasses.{root, noRefinementRoot, select, option} ]
+ [ templates.{item, defaultOption} ],
[ transformItems ]
})`;
/**
* @typedef {Object} MenuSelectCSSClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root when there are no items to display
* @property {string|string[]} [select] CSS class to add to the select element.
* @property {string|string[]} [option] CSS class to add to the option element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
+ *
*/
/**
* @typedef {Object} MenuSelectTemplates
- * @property {string|function} [header] Header template.
* @property {string|function(label: string, count: number, isRefined: boolean, value: string)} [item] Item template, provided with `label`, `count`, `isRefined` and `value` data properties.
- * @property {string} [seeAllOption='See all'] Label of the see all option in the select.
- * @property {string|function} [footer] Footer template.
- */
-
-/**
- * @typedef {Object} MenuSelectTransforms
- * @property {function} [item] Method to change the object passed to the `item` template.
+ * @property {string} [defaultOption = 'See all'] Label of the "see all" option in the select.
*/
/**
* @typedef {Object} MenuSelectWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {string} attributeName Name of the attribute for faceting
+ * @property {string} attribute Name of the attribute for faceting
* @property {string[]|function} [sortBy=['name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
* @property {MenuSelectTemplates} [templates] Customize the output through templating.
* @property {number} [limit=10] How many facets values to retrieve.
- * @property {MenuSelectTransforms} [transformData] Set of functions to update the data before passing them to the templates.
- * @property {boolean} [autoHideContainer=true] Hide the container when there are no items in the menu select.
* @property {MenuSelectCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
*/
@@ -109,51 +82,46 @@ menuSelect({
* search.addWidget(
* instantsearch.widgets.menuSelect({
* container: '#categories-menuSelect',
- * attributeName: 'hierarchicalCategories.lvl0',
+ * attribute: 'hierarchicalCategories.lvl0',
* limit: 10,
- * templates: {
- * header: 'Categories'
- * }
* })
* );
*/
export default function menuSelect({
container,
- attributeName,
+ attribute,
sortBy = ['name:asc'],
limit = 10,
cssClasses: userCssClasses = {},
templates = defaultTemplates,
- transformData,
- autoHideContainer = true,
transformItems,
}) {
- if (!container || !attributeName) {
+ if (!container || !attribute) {
throw new Error(usage);
}
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- footer: cx(bem('footer'), userCssClasses.footer),
- select: cx(bem('select'), userCssClasses.select),
- option: cx(bem('option'), userCssClasses.option),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ select: cx(suit({ descendantName: 'select' }), userCssClasses.select),
+ option: cx(suit({ descendantName: 'option' }), userCssClasses.option),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
- autoHideContainer,
renderState: {},
templates,
- transformData,
});
try {
const makeWidget = connectMenu(specializedRenderer);
- return makeWidget({ attributeName, limit, sortBy, transformItems });
- } catch (e) {
+ return makeWidget({ attribute, limit, sortBy, transformItems });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
index 20c39b0f6e..ca540b0c95 100644
--- a/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
+++ b/src/widgets/menu/__tests__/__snapshots__/menu-test.js.snap
@@ -1,23 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`menu render renders transformed items 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
@@ -59,23 +62,24 @@ exports[`menu render renders transformed items 1`] = `
`;
exports[`menu render snapshot 1`] = `
-{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "item": "{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "showMoreText": false,
},
}
}
diff --git a/src/widgets/menu/__tests__/menu-test.js b/src/widgets/menu/__tests__/menu-test.js
index 17e78aeac8..7694e50707 100644
--- a/src/widgets/menu/__tests__/menu-test.js
+++ b/src/widgets/menu/__tests__/menu-test.js
@@ -1,14 +1,38 @@
import menu from '../menu';
describe('menu', () => {
- it('throws an exception when no attributeName', () => {
+ it('throws usage when no container', () => {
+ expect(menu.bind(null, { attribute: '' })).toThrow(/^Usage/);
+ });
+
+ it('throws an exception when no attribute', () => {
const container = document.createElement('div');
expect(menu.bind(null, { container })).toThrow(/^Usage/);
});
- it('throws an exception when no container', () => {
- const attributeName = '';
- expect(menu.bind(null, { attributeName })).toThrow(/^Usage/);
+ it('throws an exception when showMoreLimit is equal to limit', () => {
+ expect(
+ menu.bind(null, {
+ attribute: 'attribute',
+ container: document.createElement('div'),
+ limit: 20,
+ showMore: true,
+ showMoreLimit: 20,
+ })
+ ).toThrow(/^Usage/);
+ });
+
+ it('throws an exception when showMoreLimit is lower than limit', () => {
+ const container = document.createElement('div');
+ expect(
+ menu.bind(null, {
+ attribute: 'attribute',
+ container,
+ limit: 20,
+ showMore: true,
+ showMoreLimit: 10,
+ })
+ ).toThrow(/^Usage/);
});
describe('render', () => {
@@ -35,7 +59,7 @@ describe('menu', () => {
it('snapshot', () => {
const widget = menu({
container: document.createElement('div'),
- attributeName: 'test',
+ attribute: 'test',
});
widget.init({
@@ -51,7 +75,7 @@ describe('menu', () => {
it('renders transformed items', () => {
const widget = menu({
container: document.createElement('div'),
- attributeName: 'test',
+ attribute: 'test',
transformItems: items =>
items.map(item => ({ ...item, transformed: true })),
});
diff --git a/src/widgets/menu/defaultTemplates.js b/src/widgets/menu/defaultTemplates.js
index 64def3bcd6..931c3e9fa9 100644
--- a/src/widgets/menu/defaultTemplates.js
+++ b/src/widgets/menu/defaultTemplates.js
@@ -1,7 +1,16 @@
/* eslint-disable max-len */
export default {
- header: '',
item:
- '{{label}} {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ',
- footer: '',
+ '' +
+ '{{label}} ' +
+ '{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} ' +
+ ' ',
+ showMoreText: `
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ `,
};
diff --git a/src/widgets/menu/menu.js b/src/widgets/menu/menu.js
index 0f5f2cd3cd..21a80904de 100644
--- a/src/widgets/menu/menu.js
+++ b/src/widgets/menu/menu.js
@@ -1,35 +1,24 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
-import defaultTemplates from './defaultTemplates.js';
-import getShowMoreConfig from '../../lib/show-more/getShowMoreConfig.js';
-import connectMenu from '../../connectors/menu/connectMenu.js';
import RefinementList from '../../components/RefinementList/RefinementList.js';
+import connectMenu from '../../connectors/menu/connectMenu.js';
+import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
- prefixKeys,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-menu');
+const suit = component('Menu');
const renderer = ({
containerNode,
cssClasses,
- collapsible,
- autoHideContainer,
renderState,
templates,
- transformData,
- showMoreConfig,
+ showMore,
}) => (
{
refine,
items,
createURL,
- canRefine,
instantSearchInstance,
isShowingMore,
toggleShowMore,
@@ -39,7 +28,6 @@ const renderer = ({
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -51,16 +39,13 @@ const renderer = ({
...facetValue,
url: createURL(facetValue.name),
}));
- const shouldAutoHideContainer = autoHideContainer && !canRefine;
render(
than the limit in the main configuration'); // eslint-disable-line
- }
-
const containerNode = getContainerNode(container);
- const showMoreLimit = (showMoreConfig && showMoreConfig.limit) || undefined;
- const showMoreTemplates =
- showMoreConfig && prefixKeys('show-more-', showMoreConfig.templates);
- const allTemplates = showMoreTemplates
- ? { ...templates, ...showMoreTemplates }
- : templates;
-
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- active: cx(bem('item', 'active'), userCssClasses.active),
- link: cx(bem('link'), userCssClasses.link),
- count: cx(bem('count'), userCssClasses.count),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ count: cx(suit({ descendantName: 'count' }), userCssClasses.count),
+ showMore: cx(suit({ descendantName: 'showMore' }), userCssClasses.showMore),
+ disabledShowMore: cx(
+ suit({ descendantName: 'showMore', modifierName: 'disabled' }),
+ userCssClasses.disabledShowMore
+ ),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
- collapsible,
- autoHideContainer,
renderState: {},
- templates: allTemplates,
- transformData,
- showMoreConfig,
+ templates,
+ showMore,
});
try {
@@ -224,13 +178,14 @@ export default function menu({
unmountComponentAtNode(containerNode)
);
return makeWidget({
- attributeName,
+ attribute,
limit,
- sortBy,
+ showMore,
showMoreLimit,
+ sortBy,
transformItems,
});
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap b/src/widgets/numeric-menu/__tests__/__snapshots__/numeric-menu-test.js.snap
similarity index 59%
rename from src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap
rename to src/widgets/numeric-menu/__tests__/__snapshots__/numeric-menu-test.js.snap
index ac3a73b639..2135bbf3d8 100644
--- a/src/widgets/numeric-refinement-list/__tests__/__snapshots__/numeric-refinement-list-test.js.snap
+++ b/src/widgets/numeric-menu/__tests__/__snapshots__/numeric-menu-test.js.snap
@@ -1,22 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`numericRefinementList() calls twice ReactDOM.render( , container) 1`] = `
- , container) 1`] = `
+
- {{label}}
+
+ {{label}}
",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
},
}
@@ -69,23 +64,23 @@ exports[`numericRefinementList() calls twice ReactDOM.render(
`;
-exports[`numericRefinementList() calls twice ReactDOM.render( , container) 2`] = `
- , container) 2`] = `
+
- {{label}}
+
+ {{label}}
",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
},
}
@@ -138,23 +128,23 @@ exports[`numericRefinementList() calls twice ReactDOM.render(
`;
-exports[`numericRefinementList() renders with transformed items 1`] = `
-
- {{label}}
+
+ {{label}}
",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
},
}
diff --git a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js b/src/widgets/numeric-menu/__tests__/numeric-menu-test.js
similarity index 76%
rename from src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js
rename to src/widgets/numeric-menu/__tests__/numeric-menu-test.js
index df46b6728c..ea4d668fa1 100644
--- a/src/widgets/numeric-refinement-list/__tests__/numeric-refinement-list-test.js
+++ b/src/widgets/numeric-menu/__tests__/numeric-menu-test.js
@@ -1,62 +1,56 @@
-import numericRefinementList from '../numeric-refinement-list.js';
+import numericMenu from '../numeric-menu.js';
const encodeValue = (start, end) =>
window.encodeURI(JSON.stringify({ start, end }));
-describe('numericRefinementList call', () => {
+describe('numericMenu call', () => {
it('throws an exception when no container', () => {
- const attributeName = '';
- const options = [];
- expect(
- numericRefinementList.bind(null, { attributeName, options })
- ).toThrow(/^Usage/);
+ const attribute = '';
+ const items = [];
+ expect(numericMenu.bind(null, { attribute, items })).toThrow(/^Usage/);
});
- it('throws an exception when no attributeName', () => {
+ it('throws an exception when no attribute', () => {
const container = document.createElement('div');
- const options = [];
- expect(numericRefinementList.bind(null, { container, options })).toThrow(
- /^Usage/
- );
+ const items = [];
+ expect(numericMenu.bind(null, { container, items })).toThrow(/^Usage/);
});
- it('throws an exception when no options', () => {
+ it('throws an exception when no items', () => {
const container = document.createElement('div');
- const attributeName = '';
- expect(
- numericRefinementList.bind(null, { attributeName, container })
- ).toThrow(/^Usage/);
+ const attribute = '';
+ expect(numericMenu.bind(null, { attribute, container })).toThrow(/^Usage/);
});
});
-describe('numericRefinementList()', () => {
+describe('numericMenu()', () => {
let ReactDOM;
let container;
let widget;
let helper;
- let options;
+ let items;
let results;
let createURL;
let state;
beforeEach(() => {
ReactDOM = { render: jest.fn() };
- numericRefinementList.__Rewire__('render', ReactDOM.render);
-
- options = [
- { name: 'All' },
- { end: 4, name: 'less than 4' },
- { start: 4, end: 4, name: '4' },
- { start: 5, end: 10, name: 'between 5 and 10' },
- { start: 10, name: 'more than 10' },
+ numericMenu.__Rewire__('render', ReactDOM.render);
+
+ items = [
+ { label: 'All' },
+ { end: 4, label: 'less than 4' },
+ { start: 4, end: 4, label: '4' },
+ { start: 5, end: 10, label: 'between 5 and 10' },
+ { start: 10, label: 'more than 10' },
];
container = document.createElement('div');
- widget = numericRefinementList({
+ widget = numericMenu({
container,
- attributeName: 'price',
- options,
+ attribute: 'price',
+ items,
cssClasses: { root: ['root', 'cx'] },
});
helper = {
@@ -94,12 +88,12 @@ describe('numericRefinementList()', () => {
});
it('renders with transformed items', () => {
- widget = numericRefinementList({
+ widget = numericMenu({
container,
- attributeName: 'price',
- options,
- transformItems: items =>
- items.map(item => ({ ...item, transformed: true })),
+ attribute: 'price',
+ items,
+ transformItems: allItems =>
+ allItems.map(item => ({ ...item, transformed: true })),
});
widget.init({ helper, instantSearchInstance: {} });
@@ -203,18 +197,18 @@ describe('numericRefinementList()', () => {
expect(helper.search).toHaveBeenCalledTimes(1, 'search called once');
});
- it('does not alter the initial options when rendering', () => {
+ it('does not alter the initial items when rendering', () => {
// Note: https://github.com/algolia/instantsearch.js/issues/1010
// Make sure we work on a copy of the initial facetValues when rendering,
// not directly editing it
// Given
- const initialOptions = [{ start: 0, end: 5, name: '1-5' }];
+ const initialOptions = [{ start: 0, end: 5, label: '1-5' }];
const initialOptionsClone = [...initialOptions];
- const testWidget = numericRefinementList({
+ const testWidget = numericMenu({
container,
- attributeName: 'price',
- options: initialOptions,
+ attribute: 'price',
+ items: initialOptions,
});
// The life cycle impose all the steps
@@ -228,8 +222,6 @@ describe('numericRefinementList()', () => {
});
afterEach(() => {
- numericRefinementList.__ResetDependency__('render');
- numericRefinementList.__ResetDependency__('autoHideContainerHOC');
- numericRefinementList.__ResetDependency__('headerFooterHOC');
+ numericMenu.__ResetDependency__('render');
});
});
diff --git a/src/widgets/numeric-refinement-list/defaultTemplates.js b/src/widgets/numeric-menu/defaultTemplates.js
similarity index 61%
rename from src/widgets/numeric-refinement-list/defaultTemplates.js
rename to src/widgets/numeric-menu/defaultTemplates.js
index f925af1dad..4bdd043731 100644
--- a/src/widgets/numeric-refinement-list/defaultTemplates.js
+++ b/src/widgets/numeric-menu/defaultTemplates.js
@@ -1,8 +1,7 @@
/* eslint-disable max-len */
export default {
- header: '',
item: `
- {{label}}
+
+ {{label}}
`,
- footer: '',
};
diff --git a/src/widgets/numeric-menu/numeric-menu.js b/src/widgets/numeric-menu/numeric-menu.js
new file mode 100644
index 0000000000..245624f745
--- /dev/null
+++ b/src/widgets/numeric-menu/numeric-menu.js
@@ -0,0 +1,169 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import RefinementList from '../../components/RefinementList/RefinementList.js';
+import connectNumericMenu from '../../connectors/numeric-menu/connectNumericMenu.js';
+import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('NumericMenu');
+
+const renderer = ({
+ containerNode,
+ attribute,
+ cssClasses,
+ renderState,
+ templates,
+}) => (
+ { createURL, instantSearchInstance, refine, items },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ renderState.templateProps = prepareTemplateProps({
+ defaultTemplates,
+ templatesConfig: instantSearchInstance.templatesConfig,
+ templates,
+ });
+ return;
+ }
+
+ render(
+ ,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+numericMenu({
+ container,
+ attribute,
+ items,
+ [ cssClasses.{root, noRefinementRoot, list, item, selectedItem, label, labelText, radio} ],
+ [ templates.{item} ],
+ [ transformItems ]
+})`;
+
+/**
+ * @typedef {Object} NumericMenuCSSClasses
+ * @property {string|string[]} [root] CSS class to add to the root element.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element when no refinements.
+ * @property {string|string[]} [list] CSS class to add to the list element.
+ * @property {string|string[]} [item] CSS class to add to each item element.
+ * @property {string|string[]} [selectedItem] CSS class to add to each selected item element.
+ * @property {string|string[]} [label] CSS class to add to each label element.
+ * @property {string|string[]} [labelText] CSS class to add to each label text element.
+ * @property {string|string[]} [radio] CSS class to add to each radio element (when using the default template).
+ */
+
+/**
+ * @typedef {Object} NumericMenuTemplates
+ * @property {string|function} [item] Item template, provided with `label` (the name in the configuration), `isRefined`, `url`, `value` (the setting for the filter) data properties.
+ */
+
+/**
+ * @typedef {Object} NumericMenuOption
+ * @property {string} label Label of the option.
+ * @property {number} [start] Low bound of the option (>=).
+ * @property {number} [end] High bound of the option (<=).
+ */
+
+/**
+ * @typedef {Object} NumericMenuWidgetOptions
+ * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
+ * @property {string} attribute Name of the attribute for filtering.
+ * @property {NumericMenuOption[]} items List of all the items.
+ * @property {NumericMenuTemplates} [templates] Templates to use for the widget.
+ * @property {NumericMenuCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * The numeric menu is a widget that displays a list of numeric filters in a list. Those numeric filters
+ * are pre-configured with creating the widget.
+ *
+ * @requirements
+ * The attribute passed to `attribute` must be declared as an [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting) in your
+ * Algolia settings.
+ *
+ * The values inside this attribute must be JavaScript numbers and not strings.
+ *
+ * @type {WidgetFactory}
+ * @devNovel NumericMenu
+ * @category filter
+ * @param {NumericMenuWidgetOptions} $0 The NumericMenu widget items
+ * @return {Widget} Creates a new instance of the NumericMenu widget.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.numericMenu({
+ * container: '#popularity',
+ * attribute: 'popularity',
+ * items: [
+ * { label: 'All' },
+ * { end: 500, label: 'less than 500' },
+ * { start: 500, end: 2000, label: 'between 500 and 2000' },
+ * { start: 2000, label: 'more than 2000' }
+ * ]
+ * })
+ * );
+ */
+export default function numericMenu({
+ container,
+ attribute,
+ items,
+ cssClasses: userCssClasses = {},
+ templates = defaultTemplates,
+ transformItems,
+} = {}) {
+ if (!container || !attribute || !items) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ radio: cx(suit({ descendantName: 'radio' }), userCssClasses.radio),
+ labelText: cx(
+ suit({ descendantName: 'labelText' }),
+ userCssClasses.labelText
+ ),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ attribute,
+ cssClasses,
+ renderState: {},
+ templates,
+ });
+ try {
+ const makeNumericMenu = connectNumericMenu(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeNumericMenu({
+ attribute,
+ items,
+ transformItems,
+ });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/numeric-refinement-list/numeric-refinement-list.js b/src/widgets/numeric-refinement-list/numeric-refinement-list.js
deleted file mode 100644
index 3d90944ab1..0000000000
--- a/src/widgets/numeric-refinement-list/numeric-refinement-list.js
+++ /dev/null
@@ -1,193 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import RefinementList from '../../components/RefinementList/RefinementList.js';
-import connectNumericRefinementList from '../../connectors/numeric-refinement-list/connectNumericRefinementList.js';
-import defaultTemplates from './defaultTemplates.js';
-
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-refinement-list');
-
-const renderer = ({
- containerNode,
- collapsible,
- autoHideContainer,
- cssClasses,
- renderState,
- transformData,
- templates,
-}) => (
- { createURL, instantSearchInstance, refine, items, hasNoResults },
- isFirstRendering
-) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- transformData,
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-numericRefinementList({
- container,
- attributeName,
- options,
- [ cssClasses.{root,header,body,footer,list,item,active,label,radio,count} ],
- [ templates.{header,item,footer} ],
- [ transformData.{item} ],
- [ autoHideContainer ],
- [ collapsible=false ],
- [ transformItems ]
-})`;
-
-/**
- * @typedef {Object} NumericRefinementListCSSClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- * @property {string|string[]} [list] CSS class to add to the list element.
- * @property {string|string[]} [label] CSS class to add to each link element.
- * @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [radio] CSS class to add to each radio element (when using the default template).
- * @property {string|string[]} [active] CSS class to add to each active element.
- */
-
-/**
- * @typedef {Object} NumericRefinementListTemplates
- * @property {string|function} [header] Header template.
- * @property {string|function} [item] Item template, provided with `label` (the name in the configuration), `isRefined`, `url`, `value` (the setting for the filter) data properties.
- * @property {string|function} [footer] Footer template.
- */
-
-/**
- * @typedef {Object} NumericRefinementListOption
- * @property {string} name Name of the option.
- * @property {number} [start] Low bound of the option (>=).
- * @property {number} [end] High bound of the option (<=).
- */
-
-/**
- * @typedef {Object} NumericRefinementListTransforms
- * @property {function({name: string, isRefined: boolean, url: string}):object} item Transforms the data for a single item to render.
- */
-
-/**
- * @typedef {Object} NumericRefinementListWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {string} attributeName Name of the attribute for filtering.
- * @property {NumericRefinementListOption[]} options List of all the options.
- * @property {NumericRefinementListTemplates} [templates] Templates to use for the widget.
- * @property {NumericRefinementListTransforms} [transformData] Functions to change the data passes to the templates. Only item can be set.
- * @property {boolean} [autoHideContainer=true] Hide the container when no results match.
- * @property {NumericRefinementListCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
- * @property {boolean|{collapsible: boolean}} [collapsible=false] Hide the widget body and footer when clicking on header.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * The numeric refinement list is a widget that displays a list of numeric filters in a list. Those numeric filters
- * are pre-configured with creating the widget.
- *
- * @requirements
- * The attribute passed to `attributeName` must be declared as an [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting) in your
- * Algolia settings.
- *
- * The values inside this attribute must be JavaScript numbers and not strings.
- *
- * @type {WidgetFactory}
- * @devNovel NumericRefinementList
- * @category filter
- * @param {NumericRefinementListWidgetOptions} $0 The NumericRefinementList widget options
- * @return {Widget} Creates a new instance of the NumericRefinementList widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.numericRefinementList({
- * container: '#popularity',
- * attributeName: 'popularity',
- * options: [
- * {name: 'All'},
- * {end: 500, name: 'less than 500'},
- * {start: 500, end: 2000, name: 'between 500 and 2000'},
- * {start: 2000, name: 'more than 2000'}
- * ],
- * templates: {
- * header: 'Popularity'
- * }
- * })
- * );
- */
-export default function numericRefinementList({
- container,
- attributeName,
- options,
- cssClasses: userCssClasses = {},
- templates = defaultTemplates,
- collapsible = false,
- transformData,
- autoHideContainer = true,
- transformItems,
-} = {}) {
- if (!container || !attributeName || !options) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- label: cx(bem('label'), userCssClasses.label),
- radio: cx(bem('radio'), userCssClasses.radio),
- active: cx(bem('item', 'active'), userCssClasses.active),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- collapsible,
- autoHideContainer,
- cssClasses,
- renderState: {},
- transformData,
- templates,
- });
- try {
- const makeNumericRefinementList = connectNumericRefinementList(
- specializedRenderer,
- () => unmountComponentAtNode(containerNode)
- );
- return makeNumericRefinementList({
- attributeName,
- options,
- transformItems,
- });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/numeric-selector/__tests__/__snapshots__/numeric-selector-test.js.snap b/src/widgets/numeric-selector/__tests__/__snapshots__/numeric-selector-test.js.snap
deleted file mode 100644
index 4c50c72b13..0000000000
--- a/src/widgets/numeric-selector/__tests__/__snapshots__/numeric-selector-test.js.snap
+++ /dev/null
@@ -1,82 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`numericSelector() calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`numericSelector() calls twice ReactDOM.render( , container) 2`] = `
-
-`;
-
-exports[`numericSelector() computes refined values and pass them to 1`] = `
-
-`;
diff --git a/src/widgets/numeric-selector/__tests__/numeric-selector-test.js b/src/widgets/numeric-selector/__tests__/numeric-selector-test.js
deleted file mode 100644
index ea63e059de..0000000000
--- a/src/widgets/numeric-selector/__tests__/numeric-selector-test.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import expect from 'expect';
-import sinon from 'sinon';
-import numericSelector from '../numeric-selector';
-
-describe('numericSelector()', () => {
- let ReactDOM;
- let container;
- let options;
- let cssClasses;
- let widget;
- let expectedProps;
- let helper;
- let results;
-
- beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
-
- numericSelector.__Rewire__('render', ReactDOM.render);
-
- container = document.createElement('div');
- options = [{ value: 1, label: 'first' }, { value: 2, label: 'second' }];
- cssClasses = {
- root: ['custom-root', 'cx'],
- select: 'custom-select',
- item: 'custom-item',
- };
- widget = numericSelector({
- container,
- options,
- attributeName: 'aNumAttr',
- cssClasses,
- });
- expectedProps = {
- shouldAutoHideContainer: false,
- cssClasses: {
- root: 'ais-numeric-selector custom-root cx',
- select: 'ais-numeric-selector custom-select',
- item: 'ais-numeric-selector--item custom-item',
- },
- currentValue: 1,
- options: [{ value: 1, label: 'first' }, { value: 2, label: 'second' }],
- setValue: () => {},
- };
- helper = {
- addNumericRefinement: sinon.spy(),
- clearRefinements: sinon.spy(),
- search: sinon.spy(),
- };
- results = {
- hits: [],
- nbHits: 0,
- };
- widget.init({ helper });
- helper.addNumericRefinement.resetHistory();
- });
-
- it('configures the right numericRefinement', () => {
- expect(widget.getConfiguration({}, {})).toEqual({
- numericRefinements: {
- aNumAttr: {
- '=': [1],
- },
- },
- });
- });
-
- it('configures the right numericRefinement when present in the url', () => {
- const urlState = {
- numericRefinements: {
- aNumAttr: {
- '=': [2],
- },
- },
- };
- expect(widget.getConfiguration({}, urlState)).toEqual({
- numericRefinements: {
- aNumAttr: {
- '=': [2],
- },
- },
- });
- });
-
- it('calls twice ReactDOM.render( , container)', () => {
- widget.render({ helper, results, state: helper.state });
- widget.render({ helper, results, state: helper.state });
-
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
- });
-
- it('computes refined values and pass them to ', () => {
- helper.state = {
- numericRefinements: {
- aNumAttr: {
- '=': [20],
- },
- },
- };
- expectedProps.currentValue = 20;
- widget.render({ helper, results, state: helper.state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- });
-
- it('sets the underlying numeric refinement', () => {
- widget._refine(2);
- expect(helper.addNumericRefinement.calledOnce).toBe(
- true,
- 'addNumericRefinement called once'
- );
- expect(helper.search.calledOnce).toBe(true, 'search called once');
- });
-
- it('cancels the underlying numeric refinement', () => {
- widget._refine(undefined);
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'clearRefinements called once'
- );
- expect(helper.addNumericRefinement.called).toBe(
- false,
- 'addNumericRefinement never called'
- );
- expect(helper.search.calledOnce).toBe(true, 'search called once');
- });
-
- afterEach(() => {
- numericSelector.__ResetDependency__('render');
- });
-});
diff --git a/src/widgets/numeric-selector/numeric-selector.js b/src/widgets/numeric-selector/numeric-selector.js
deleted file mode 100644
index 7c7d676b72..0000000000
--- a/src/widgets/numeric-selector/numeric-selector.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import Selector from '../../components/Selector.js';
-import connectNumericSelector from '../../connectors/numeric-selector/connectNumericSelector.js';
-
-import { bemHelper, getContainerNode } from '../../lib/utils.js';
-
-const bem = bemHelper('ais-numeric-selector');
-
-const renderer = ({ containerNode, autoHideContainer, cssClasses }) => (
- { currentRefinement, refine, hasNoResults, options },
- isFirstRendering
-) => {
- if (isFirstRendering) return;
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage: numericSelector({
- container,
- attributeName,
- options,
- cssClasses.{root,select,item},
- autoHideContainer,
- transformItems
-})`;
-
-/**
- * @typedef {Object} NumericOption
- * @property {number} value The numerical value to refine with.
- * If the value is `undefined` or `"undefined"`, the option resets the filter.
- * @property {string} label Label to display in the option.
- */
-
-/**
- * @typedef {Object} NumericSelectorCSSClasses
- * @property {string|string[]} [root] CSS classes added to the outer ``.
- * @property {string|string[]} [select] CSS classes added to the parent `
`.
- * @property {string|string[]} [item] CSS classes added to each ``.
- */
-
-/**
- * @typedef {Object} NumericSelectorWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {string} attributeName Name of the numeric attribute to use.
- * @property {NumericOption[]} options Array of objects defining the different values and labels.
- * @property {string} [operator='='] The operator to use to refine.
- * @property {boolean} [autoHideContainer=false] Hide the container when no results match.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- * @property {NumericSelectorCSSClasses} [cssClasses] CSS classes to be added.
- */
-
-/**
- * This widget lets the user choose between numerical refinements from a dropdown menu.
- *
- * @requirements
- * The attribute passed to `attributeName` must be declared as an
- * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
- * in your Algolia settings.
- *
- * The values inside this attribute must be JavaScript numbers and not strings.
- * @type {WidgetFactory}
- * @devNovel NumericSelector
- * @category filter
- * @param {NumericSelectorWidgetOptions} $0 The NumericSelector widget options.
- * @return {Widget} A new instance of NumericSelector widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.numericSelector({
- * container: '#rating-selector',
- * attributeName: 'rating',
- * operator: '=',
- * options: [
- * {label: 'All products'},
- * {label: 'Only 5 star products', value: 5},
- * {label: 'Only 4 star products', value: 4},
- * {label: 'Only 3 star products', value: 3},
- * {label: 'Only 2 star products', value: 2},
- * {label: 'Only 1 star products', value: 1},
- * ]
- * })
- * );
- */
-export default function numericSelector({
- container,
- operator = '=',
- attributeName,
- options,
- cssClasses: userCssClasses = {},
- autoHideContainer = false,
- transformItems,
-}) {
- const containerNode = getContainerNode(container);
- if (!container || !options || options.length === 0 || !attributeName) {
- throw new Error(usage);
- }
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- // We use the same class to avoid regression on existing website. It needs to be replaced
- // eventually by `bem('select')
- select: cx(bem(null), userCssClasses.select),
- item: cx(bem('item'), userCssClasses.item),
- };
-
- const specializedRenderer = renderer({
- autoHideContainer,
- containerNode,
- cssClasses,
- });
-
- try {
- const makeNumericSelector = connectNumericSelector(
- specializedRenderer,
- () => unmountComponentAtNode(containerNode)
- );
- return makeNumericSelector({
- operator,
- attributeName,
- options,
- transformItems,
- });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/pagination/__tests__/__snapshots__/pagination-test.js.snap b/src/widgets/pagination/__tests__/__snapshots__/pagination-test.js.snap
index 9c96c27af0..961e51a0c9 100644
--- a/src/widgets/pagination/__tests__/__snapshots__/pagination-test.js.snap
+++ b/src/widgets/pagination/__tests__/__snapshots__/pagination-test.js.snap
@@ -1,33 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pagination() calls twice ReactDOM.render( , container) 1`] = `
- , containe
]
}
setCurrentPage={[Function]}
- shouldAutoHideContainer={false}
- showFirstLast={true}
+ showFirst={true}
+ showLast={true}
+ showNext={true}
+ showPrevious={true}
+ templates={
+ Object {
+ "first": "«",
+ "last": "»",
+ "next": "›",
+ "previous": "‹",
+ }
+ }
/>
`;
exports[`pagination() calls twice ReactDOM.render( , container) 2`] = `
- , containe
]
}
setCurrentPage={[Function]}
- shouldAutoHideContainer={false}
- showFirstLast={true}
+ showFirst={true}
+ showLast={true}
+ showNext={true}
+ showPrevious={true}
+ templates={
+ Object {
+ "first": "«",
+ "last": "»",
+ "next": "›",
+ "previous": "‹",
+ }
+ }
/>
`;
diff --git a/src/widgets/pagination/__tests__/pagination-test.js b/src/widgets/pagination/__tests__/pagination-test.js
index 8cbb7b6313..47776aeabe 100644
--- a/src/widgets/pagination/__tests__/pagination-test.js
+++ b/src/widgets/pagination/__tests__/pagination-test.js
@@ -1,5 +1,3 @@
-import expect from 'expect';
-import sinon from 'sinon';
import pagination from '../pagination';
describe('pagination call', () => {
@@ -7,6 +5,7 @@ describe('pagination call', () => {
expect(pagination.bind(null)).toThrow(/^Usage/);
});
});
+
describe('pagination()', () => {
let ReactDOM;
let container;
@@ -16,21 +15,23 @@ describe('pagination()', () => {
let cssClasses;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
pagination.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
cssClasses = {
- root: ['root', 'cx'],
+ root: ['root', 'customRoot'],
+ noRefinementRoot: 'noRefinementRoot',
+ list: 'list',
item: 'item',
+ firstPageItem: 'firstPageItem',
+ lastPageItem: 'lastPageItem',
+ previousPageItem: 'previousPageItem',
+ nextPageItem: 'nextPageItem',
+ pageItem: 'pageItem',
+ selectedItem: 'selectedItem',
+ disabledItem: 'disabledItem',
link: 'link',
- page: 'page',
- previous: 'previous',
- next: 'next',
- first: 'first',
- last: 'last',
- active: 'active',
- disabled: 'disabled',
};
widget = pagination({ container, scrollTo: false, cssClasses });
results = {
@@ -40,8 +41,8 @@ describe('pagination()', () => {
nbPages: 20,
};
helper = {
- setPage: sinon.spy(),
- search: sinon.spy(),
+ setPage: jest.fn(),
+ search: jest.fn(),
getPage: () => 0,
};
widget.init({ helper });
@@ -53,30 +54,27 @@ describe('pagination()', () => {
it('sets the page', () => {
widget.refine(helper, 42);
- expect(helper.setPage.calledOnce).toBe(true);
- expect(helper.search.calledOnce).toBe(true);
+ expect(helper.setPage).toHaveBeenCalledTimes(1);
+ expect(helper.search).toHaveBeenCalledTimes(1);
});
it('calls twice ReactDOM.render( , container)', () => {
widget.render({ results, helper, state: { page: 0 } });
widget.render({ results, helper, state: { page: 0 } });
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
});
describe('mocking getContainerNode', () => {
let scrollIntoView;
beforeEach(() => {
- scrollIntoView = sinon.spy();
- const getContainerNode = sinon.stub().returns({
+ scrollIntoView = jest.fn();
+ const getContainerNode = jest.fn().mockReturnValue({
scrollIntoView,
});
pagination.__Rewire__('getContainerNode', getContainerNode);
@@ -86,10 +84,7 @@ describe('pagination()', () => {
widget = pagination({ container, scrollTo: false });
widget.init({ helper });
widget.refine(helper, 2);
- expect(scrollIntoView.calledOnce).toBe(
- false,
- 'scrollIntoView never called'
- );
+ expect(scrollIntoView).toHaveBeenCalledTimes(0);
});
it('should scroll to body', () => {
@@ -98,12 +93,9 @@ describe('pagination()', () => {
widget.render({ results, helper, state: { page: 0 } });
const {
props: { setCurrentPage },
- } = ReactDOM.render.firstCall.args[0];
+ } = ReactDOM.render.mock.calls[0][0];
setCurrentPage(2);
- expect(scrollIntoView.calledOnce).toBe(
- true,
- 'scrollIntoView called once'
- );
+ expect(scrollIntoView).toHaveBeenCalledTimes(1);
});
afterEach(() => {
@@ -113,7 +105,6 @@ describe('pagination()', () => {
afterEach(() => {
pagination.__ResetDependency__('render');
- pagination.__ResetDependency__('autoHideContainerHOC');
});
});
@@ -126,21 +117,23 @@ describe('pagination MaxPage', () => {
let paginationOptions;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
pagination.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
cssClasses = {
root: 'root',
+ noRefinementRoot: 'noRefinementRoot',
+ list: 'list',
item: 'item',
+ firstPageItem: 'firstPageItem',
+ lastPageItem: 'lastPageItem',
+ previousPageItem: 'previousPageItem',
+ nextPageItem: 'nextPageItem',
+ pageItem: 'pageItem',
+ selectedItem: 'selectedItem',
+ disabledItem: 'disabledItem',
link: 'link',
- page: 'page',
- previous: 'previous',
- next: 'next',
- first: 'first',
- last: 'last',
- active: 'active',
- disabled: 'disabled',
};
results = {
hits: [{ first: 'hit', second: 'hit' }],
@@ -156,14 +149,14 @@ describe('pagination MaxPage', () => {
expect(widget.getMaxPage(results)).toEqual(30);
});
- it('does reduce the number of page if lower than nbPages', () => {
- paginationOptions.maxPages = 20;
+ it('does reduce the number of pages if lower than nbPages', () => {
+ paginationOptions.totalPages = 20;
widget = pagination(paginationOptions);
expect(widget.getMaxPage(results)).toEqual(20);
});
- it('does not reduce the number of page if greater than nbPages', () => {
- paginationOptions.maxPages = 40;
+ it('does not reduce the number of pages if greater than nbPages', () => {
+ paginationOptions.totalPages = 40;
widget = pagination(paginationOptions);
expect(widget.getMaxPage(results)).toEqual(30);
});
diff --git a/src/widgets/pagination/pagination.js b/src/widgets/pagination/pagination.js
index b4a1e872a8..8687ce1e0f 100644
--- a/src/widgets/pagination/pagination.js
+++ b/src/widgets/pagination/pagination.js
@@ -1,28 +1,28 @@
-import defaults from 'lodash/defaults';
-
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
import Pagination from '../../components/Pagination/Pagination.js';
import connectPagination from '../../connectors/pagination/connectPagination.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import { bemHelper, getContainerNode } from '../../lib/utils.js';
+const suit = component('Pagination');
-const defaultLabels = {
+const defaultTemplates = {
previous: '‹',
next: '›',
first: '«',
last: '»',
};
-const bem = bemHelper('ais-pagination');
-
const renderer = ({
containerNode,
cssClasses,
- labels,
- showFirstLast,
- autoHideContainer,
+ templates,
+ totalPages,
+ showFirst,
+ showLast,
+ showPrevious,
+ showNext,
scrollToNode,
}) => (
{
@@ -47,22 +47,23 @@ const renderer = ({
}
};
- const shouldAutoHideContainer = autoHideContainer && nbHits === 0;
-
render(
,
containerNode
);
@@ -71,31 +72,35 @@ const renderer = ({
const usage = `Usage:
pagination({
container,
- [ cssClasses.{root,item,page,previous,next,first,last,active,disabled}={} ],
- [ labels.{previous,next,first,last} ],
- [ maxPages ],
- [ padding=3 ],
- [ showFirstLast=true ],
- [ autoHideContainer=true ],
- [ scrollTo='body' ]
+ [ totalPages ],
+ [ padding = 3 ],
+ [ showFirst = true ],
+ [ showLast = true ],
+ [ showPrevious = true ],
+ [ showNext = true ],
+ [ scrollTo = 'body' ]
+ [ templates.{previous, next, first, last} ],
+ [ cssClasses.{root, noRefinementRoot, list, item, itemFirstPage, itemLastPage, itemPreviousPage, itemNextPage, itemPage, selectedItem, disabledItem, link} ],
})`;
/**
* @typedef {Object} PaginationCSSClasses
- * @property {string|string[]} [root] CSS classes added to the parent ``.
+ * @property {string|string[]} [root] CSS classes added to the root element of the widget.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element of the widget if there are no refinements.
+ * @property {string|string[]} [list] CSS classes added to the wrapping ``.
* @property {string|string[]} [item] CSS classes added to each ``.
+ * @property {string|string[]} [itemFirstPage] CSS classes added to the first ` `.
+ * @property {string|string[]} [itemLastPage] CSS classes added to the last ` `.
+ * @property {string|string[]} [itemPreviousPage] CSS classes added to the previous ` `.
+ * @property {string|string[]} [itemNextPage] CSS classes added to the next ` `.
+ * @property {string|string[]} [itemPage] CSS classes added to page ` `.
+ * @property {string|string[]} [selectedItem] CSS classes added to the selected ` `.
+ * @property {string|string[]} [disabledItem] CSS classes added to the disabled ` `.
* @property {string|string[]} [link] CSS classes added to each link.
- * @property {string|string[]} [page] CSS classes added to page ` `.
- * @property {string|string[]} [previous] CSS classes added to the previous ` `.
- * @property {string|string[]} [next] CSS classes added to the next ` `.
- * @property {string|string[]} [first] CSS classes added to the first ` `.
- * @property {string|string[]} [last] CSS classes added to the last ` `.
- * @property {string|string[]} [active] CSS classes added to the active ` `.
- * @property {string|string[]} [disabled] CSS classes added to the disabled ` `.
*/
/**
- * @typedef {Object} PaginationLabels
+ * @typedef {Object} PaginationTemplates
* @property {string} [previous] Label for the Previous link.
* @property {string} [next] Label for the Next link.
* @property {string} [first] Label for the First link.
@@ -105,12 +110,14 @@ pagination({
/**
* @typedef {Object} PaginationWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {number} [maxPages] The max number of pages to browse.
+ * @property {number} [totalPages] The max number of pages to browse.
* @property {number} [padding=3] The number of pages to display on each side of the current page.
* @property {string|HTMLElement|boolean} [scrollTo='body'] Where to scroll after a click, set to `false` to disable.
- * @property {boolean} [showFirstLast=true] Define if the First and Last links should be displayed.
- * @property {boolean} [autoHideContainer=true] Hide the container when no results match.
- * @property {PaginationLabels} [labels] Text to display in the various links (prev, next, first, last).
+ * @property {boolean} [showFirst=true] Whether to show the “first page” control
+ * @property {boolean} [showLast=true] Whether to show the last page” control
+ * @property {boolean} [showNext=true] Whether to show the “next page” control
+ * @property {boolean} [showPrevious=true] Whether to show the “previous page” control
+ * @property {PaginationTemplates} [templates] Text to display in the links.
* @property {PaginationCSSClasses} [cssClasses] CSS classes to be added.
*/
@@ -134,21 +141,24 @@ pagination({
* search.addWidget(
* instantsearch.widgets.pagination({
* container: '#pagination-container',
- * maxPages: 20,
+ * totalPages: 20,
* // default is to scroll to 'body', here we disable this behavior
* scrollTo: false,
- * showFirstLast: false,
+ * showFirst: false,
+ * showLast: false,
* })
* );
*/
export default function pagination({
container,
- labels: userLabels = defaultLabels,
+ templates: userTemplates = {},
cssClasses: userCssClasses = {},
- maxPages,
+ totalPages,
padding,
- showFirstLast = true,
- autoHideContainer = true,
+ showFirst = true,
+ showLast = true,
+ showPrevious = true,
+ showNext = true,
scrollTo: userScrollTo = 'body',
} = {}) {
if (!container) {
@@ -161,27 +171,55 @@ export default function pagination({
const scrollToNode = scrollTo !== false ? getContainerNode(scrollTo) : false;
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- item: cx(bem('item'), userCssClasses.item),
- link: cx(bem('link'), userCssClasses.link),
- page: cx(bem('item', 'page'), userCssClasses.page),
- previous: cx(bem('item', 'previous'), userCssClasses.previous),
- next: cx(bem('item', 'next'), userCssClasses.next),
- first: cx(bem('item', 'first'), userCssClasses.first),
- last: cx(bem('item', 'last'), userCssClasses.last),
- active: cx(bem('item', 'active'), userCssClasses.active),
- disabled: cx(bem('item', 'disabled'), userCssClasses.disabled),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ firstPageItem: cx(
+ suit({ descendantName: 'item', modifierName: 'firstPage' }),
+ userCssClasses.firstPageItem
+ ),
+ lastPageItem: cx(
+ suit({ descendantName: 'item', modifierName: 'lastPage' }),
+ userCssClasses.lastPageItem
+ ),
+ previousPageItem: cx(
+ suit({ descendantName: 'item', modifierName: 'previousPage' }),
+ userCssClasses.previousPageItem
+ ),
+ nextPageItem: cx(
+ suit({ descendantName: 'item', modifierName: 'nextPage' }),
+ userCssClasses.nextPageItem
+ ),
+ pageItem: cx(
+ suit({ descendantName: 'item', modifierName: 'page' }),
+ userCssClasses.pageItem
+ ),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ disabledItem: cx(
+ suit({ descendantName: 'item', modifierName: 'disabled' }),
+ userCssClasses.disabledItem
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
};
- const labels = defaults(userLabels, defaultLabels);
+ const templates = { ...defaultTemplates, ...userTemplates };
const specializedRenderer = renderer({
containerNode,
cssClasses,
- labels,
- showFirstLast,
+ templates,
+ showFirst,
+ showLast,
+ showPrevious,
+ showNext,
padding,
- autoHideContainer,
scrollToNode,
});
@@ -189,8 +227,8 @@ export default function pagination({
const makeWidget = connectPagination(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeWidget({ maxPages, padding });
- } catch (e) {
+ return makeWidget({ totalPages, padding });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/panel/__tests__/panel-test.js b/src/widgets/panel/__tests__/panel-test.js
new file mode 100644
index 0000000000..57ece7bb01
--- /dev/null
+++ b/src/widgets/panel/__tests__/panel-test.js
@@ -0,0 +1,46 @@
+import panel from '../panel';
+
+describe('panel call', () => {
+ test('without arguments does not throw', () => {
+ expect(() => panel()).not.toThrow();
+ });
+
+ test('with templates does not throw', () => {
+ expect(() =>
+ panel({
+ templates: { header: 'header' },
+ })
+ ).not.toThrow();
+ });
+
+ test('with `hidden` as function does not throw', () => {
+ expect(() =>
+ panel({
+ hidden: () => true,
+ })
+ ).not.toThrow();
+ });
+
+ test('with `hidden` as boolean warns', () => {
+ const warn = jest.spyOn(global.console, 'warn');
+ warn.mockImplementation(() => {});
+
+ panel({
+ hidden: true,
+ });
+
+ expect(warn).toHaveBeenCalledWith(
+ '[InstantSearch.js]: The `hidden` option in the "panel" widget expects a function returning a boolean (received "boolean" type).'
+ );
+
+ warn.mockRestore();
+ });
+
+ test('with a widget without `container` throws', () => {
+ const fakeWidget = () => {};
+
+ expect(() => panel()(fakeWidget)({})).toThrowErrorMatchingInlineSnapshot(
+ `"[InstantSearch.js] The \`container\` option is required in the widget within the panel."`
+ );
+ });
+});
diff --git a/src/widgets/panel/panel.js b/src/widgets/panel/panel.js
new file mode 100644
index 0000000000..7a0f76a5af
--- /dev/null
+++ b/src/widgets/panel/panel.js
@@ -0,0 +1,158 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import { getContainerNode, prepareTemplateProps, warn } from '../../lib/utils';
+import { component } from '../../lib/suit';
+import Panel from '../../components/Panel/Panel';
+
+const suit = component('Panel');
+
+const renderer = ({ containerNode, cssClasses, templateProps }) => ({
+ options,
+ hidden,
+}) => {
+ let bodyRef = null;
+
+ render(
+ (bodyRef = ref)}
+ />,
+ containerNode
+ );
+
+ return { bodyRef };
+};
+
+const usage = `Usage:
+const widgetWithHeaderFooter = panel({
+ [ templates.{header, footer} ],
+ [ hidden ],
+ [ cssClasses.{root, noRefinementRoot, body, header, footer} ],
+})(widget);
+
+const myWidget = widgetWithHeaderFooter(widgetOptions)`;
+
+/**
+ * @typedef {Object} PanelWidgetCSSClasses
+ * @property {string|string[]} [root] CSS classes added to the root element of the widget.
+ * @property {string|string[]} [noRefinementRoot] CSS classes added to the root element of the widget when there's no refinements.
+ * @property {string|string[]} [header] CSS class to add to the header.
+ * @property {string|string[]} [footer] CSS class to add to the SVG footer.
+ */
+
+/**
+ * @typedef {Object} PanelTemplates
+ * @property {string|function} [header = ''] Template to use for the header.
+ * @property {string|function} [footer = ''] Template to use for the footer.
+ */
+
+/**
+ * @typedef {Object} PanelWidgetOptions
+ * @property {function} [hidden] This function is called on each render to determine from the render options if the panel have to be hidden or not. If the value is `true` the CSS class `noRefinementRoot` is applied and the wrapper is hidden.
+ * @property {PanelTemplates} [templates] Templates to use for the widgets.
+ * @property {PanelWidgetCSSClasses} [cssClasses] CSS classes to add.
+ */
+
+/**
+ * The panel widget wraps other widgets in a consistent panel design. It also reacts, indicates and sets CSS classes when widgets are no more relevant for refining.
+ *
+ * @type {WidgetFactory}
+ * @devNovel Panel
+ * @category metadata
+ * @param {PanelWidgetOptions} $0 Panel widget options.
+ * @return {function} A new panel widget instance
+ * @example
+ * const refinementListWithPanel = instantsearch.widgets.panel({
+ * templates: {
+ * header: 'Brand',
+ * },
+ * })(instantsearch.widgets.refinementList);
+ *
+ * search.addWidget(
+ * refinementListWithPanel({
+ * container: '#refinement-list',
+ * attribute: 'brand',
+ * })
+ * );
+ */
+export default function panel({
+ templates = {},
+ hidden = () => false,
+ cssClasses: userCssClasses = {},
+} = {}) {
+ if (typeof hidden !== 'function') {
+ warn(
+ `The \`hidden\` option in the "panel" widget expects a function returning a boolean (received "${typeof hidden}" type).`
+ );
+ }
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ body: cx(suit({ descendantName: 'body' }), userCssClasses.body),
+ header: cx(suit({ descendantName: 'header' }), userCssClasses.header),
+ footer: cx(suit({ descendantName: 'footer' }), userCssClasses.footer),
+ };
+
+ return widgetFactory => (widgetOptions = {}) => {
+ const { container } = widgetOptions;
+
+ if (!container) {
+ throw new Error(
+ `[InstantSearch.js] The \`container\` option is required in the widget within the panel.`
+ );
+ }
+
+ const defaultTemplates = { header: '', footer: '' };
+ const templateProps = prepareTemplateProps({ defaultTemplates, templates });
+
+ const renderPanel = renderer({
+ containerNode: getContainerNode(container),
+ cssClasses,
+ templateProps,
+ });
+
+ try {
+ const { bodyRef } = renderPanel({
+ options: {},
+ hidden: true,
+ });
+
+ const widget = widgetFactory({
+ ...widgetOptions,
+ container: getContainerNode(bodyRef),
+ });
+
+ return {
+ ...widget,
+ dispose(...args) {
+ unmountComponentAtNode(getContainerNode(container));
+
+ if (typeof widget.dispose === 'function') {
+ widget.dispose.call(this, ...args);
+ }
+ },
+ render(...args) {
+ const [options] = args;
+
+ renderPanel({
+ options,
+ hidden: Boolean(hidden(options)),
+ });
+
+ if (typeof widget.render === 'function') {
+ widget.render.call(this, ...args);
+ }
+ },
+ };
+ } catch (error) {
+ throw new Error(usage);
+ }
+ };
+}
diff --git a/src/widgets/powered-by/__tests__/__snapshots__/powered-by-test.js.snap b/src/widgets/powered-by/__tests__/__snapshots__/powered-by-test.js.snap
new file mode 100644
index 0000000000..f97031387d
--- /dev/null
+++ b/src/widgets/powered-by/__tests__/__snapshots__/powered-by-test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`poweredBy renders only once at init 1`] = `
+
+`;
diff --git a/src/widgets/powered-by/__tests__/powered-by-test.js b/src/widgets/powered-by/__tests__/powered-by-test.js
new file mode 100644
index 0000000000..2a2ee7e7f7
--- /dev/null
+++ b/src/widgets/powered-by/__tests__/powered-by-test.js
@@ -0,0 +1,46 @@
+import poweredBy from '../powered-by';
+
+describe('poweredBy call', () => {
+ it('throws an exception when no container', () => {
+ expect(poweredBy).toThrow();
+ });
+});
+
+describe('poweredBy', () => {
+ let ReactDOM;
+ let widget;
+ let container;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ poweredBy.__Rewire__('render', ReactDOM.render);
+
+ container = document.createElement('div');
+ widget = poweredBy({
+ container,
+ cssClasses: {
+ root: 'root',
+ link: 'link',
+ logo: 'logo',
+ },
+ });
+
+ widget.init({});
+ });
+
+ afterEach(() => {
+ poweredBy.__ResetDependency__('render');
+ });
+
+ it('configures nothing', () => {
+ expect(widget.getConfiguration).toEqual(undefined);
+ });
+
+ it('renders only once at init', () => {
+ widget.render({});
+ widget.render({});
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ });
+});
diff --git a/src/widgets/powered-by/powered-by.js b/src/widgets/powered-by/powered-by.js
new file mode 100644
index 0000000000..fbf27b179e
--- /dev/null
+++ b/src/widgets/powered-by/powered-by.js
@@ -0,0 +1,96 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import PoweredBy from '../../components/PoweredBy/PoweredBy';
+import connectPoweredBy from '../../connectors/powered-by/connectPoweredBy.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
+
+const suit = component('PoweredBy');
+
+const renderer = ({ containerNode, cssClasses }) => (
+ { url, widgetParams },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ const { theme } = widgetParams;
+
+ render(
+ ,
+ containerNode
+ );
+
+ return;
+ }
+};
+
+const usage = `Usage:
+poweredBy({
+ container,
+ [ theme = 'light' ],
+})`;
+
+/**
+ * @typedef {Object} PoweredByWidgetCssClasses
+ * @property {string|string[]} [root] CSS classes added to the root element of the widget.
+ * @property {string|string[]} [link] CSS class to add to the link.
+ * @property {string|string[]} [logo] CSS class to add to the SVG logo.
+ */
+
+/**
+ * @typedef {Object} PoweredByWidgetOptions
+ * @property {string|HTMLElement} container Place where to insert the widget in your webpage.
+ * @property {string} [theme] The theme of the logo ("light" or "dark").
+ * @property {PoweredByWidgetCssClasses} [cssClasses] CSS classes to add.
+ */
+
+/**
+ * The `poweredBy` widget is used to display the logo to redirect to Algolia.
+ * @type {WidgetFactory}
+ * @devNovel PoweredBy
+ * @category metadata
+ * @param {PoweredByWidgetOptions} $0 PoweredBy widget options. Some keys are mandatory: `container`,
+ * @return {Widget} A new poweredBy widget instance
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.poweredBy({
+ * container: '#poweredBy-container',
+ * theme: 'dark',
+ * })
+ * );
+ */
+export default function poweredBy({
+ container,
+ cssClasses: userCssClasses = {},
+ theme = 'light',
+} = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(
+ suit(),
+ suit({ modifierName: theme === 'dark' ? 'dark' : 'light' }),
+ userCssClasses.root
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
+ logo: cx(suit({ descendantName: 'logo' }), userCssClasses.logo),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ });
+
+ try {
+ const makeWidget = connectPoweredBy(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+
+ return makeWidget({ theme });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/price-ranges/__tests__/__snapshots__/price-ranges-test.js.snap b/src/widgets/price-ranges/__tests__/__snapshots__/price-ranges-test.js.snap
deleted file mode 100644
index b069138915..0000000000
--- a/src/widgets/price-ranges/__tests__/__snapshots__/price-ranges-test.js.snap
+++ /dev/null
@@ -1,215 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`priceRanges() without refinements calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`priceRanges() without refinements calls twice ReactDOM.render( , container) 2`] = `
-
-`;
diff --git a/src/widgets/price-ranges/__tests__/price-ranges-test.js b/src/widgets/price-ranges/__tests__/price-ranges-test.js
deleted file mode 100644
index 559da98ffd..0000000000
--- a/src/widgets/price-ranges/__tests__/price-ranges-test.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import expect from 'expect';
-import sinon from 'sinon';
-import priceRanges from '../price-ranges.js';
-
-const instantSearchInstance = { templatesConfig: undefined };
-
-describe('priceRanges call', () => {
- it('throws an exception when no container', () => {
- const attributeName = '';
- expect(priceRanges.bind(null, { attributeName })).toThrow(/^Usage:/);
- });
-
- it('throws an exception when no attributeName', () => {
- const container = document.createElement('div');
- expect(priceRanges.bind(null, { container })).toThrow(/^Usage:/);
- });
-});
-
-describe('priceRanges()', () => {
- let ReactDOM;
- let container;
- let widget;
- let results;
- let helper;
- let state;
- let createURL;
-
- beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
-
- priceRanges.__Rewire__('render', ReactDOM.render);
-
- container = document.createElement('div');
- widget = priceRanges({
- container,
- attributeName: 'aNumAttr',
- cssClasses: { root: ['root', 'cx'] },
- });
- results = {
- hits: [1],
- nbHits: 1,
- getFacetStats: sinon.stub().returns({
- min: 1.99,
- max: 4999.98,
- avg: 243.349,
- sum: 2433490.0,
- }),
- };
- });
-
- it('adds the attribute as a facet', () => {
- expect(widget.getConfiguration()).toEqual({ facets: ['aNumAttr'] });
- });
-
- describe('without refinements', () => {
- beforeEach(() => {
- helper = {
- getRefinements: sinon.stub().returns([]),
- clearRefinements: sinon.spy(),
- addNumericRefinement: sinon.spy(),
- search: sinon.spy(),
- };
-
- state = {
- clearRefinements: sinon.stub().returnsThis(),
- addNumericRefinement: sinon.stub().returnsThis(),
- };
-
- createURL = sinon.stub().returns('#createURL');
-
- widget.init({ helper, instantSearchInstance });
- });
-
- it('calls twice ReactDOM.render( , container)', () => {
- widget.render({ results, helper, state, createURL });
- widget.render({ results, helper, state, createURL });
-
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
- });
-
- it('calls getRefinements to check if there are some refinements', () => {
- widget.render({ results, helper, state, createURL });
- expect(helper.getRefinements.calledOnce).toBe(
- true,
- 'getRefinements called once'
- );
- });
-
- it('refines on the lower bound', () => {
- widget.refine({ from: 10, to: undefined });
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'helper.clearRefinements called once'
- );
- expect(helper.addNumericRefinement.calledOnce).toBe(
- true,
- 'helper.addNumericRefinement called once'
- );
- expect(helper.search.calledOnce).toBe(true, 'helper.search called once');
- });
-
- it('refines on the upper bound', () => {
- widget.refine({ from: undefined, to: 10 });
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'helper.clearRefinements called once'
- );
- expect(helper.search.calledOnce).toBe(true, 'helper.search called once');
- });
-
- it('refines on the 2 bounds', () => {
- widget.refine({ from: 10, to: 20 });
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'helper.clearRefinements called once'
- );
- expect(helper.addNumericRefinement.calledTwice).toBe(
- true,
- 'helper.addNumericRefinement called twice'
- );
- expect(helper.search.calledOnce).toBe(true, 'helper.search called once');
- });
- });
-
- afterEach(() => {
- priceRanges.__ResetDependency__('render');
- priceRanges.__ResetDependency__('autoHideContainerHOC');
- priceRanges.__ResetDependency__('headerFooterHOC');
- });
-});
diff --git a/src/widgets/price-ranges/defaultTemplates.js b/src/widgets/price-ranges/defaultTemplates.js
deleted file mode 100644
index 6ed9bcd819..0000000000
--- a/src/widgets/price-ranges/defaultTemplates.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default {
- header: '',
- item: `
- {{#from}}
- {{^to}}
- ≥
- {{/to}}
- {{currency}}{{#helpers.formatNumber}}{{from}}{{/helpers.formatNumber}}
- {{/from}}
- {{#to}}
- {{#from}}
- -
- {{/from}}
- {{^from}}
- ≤
- {{/from}}
- {{#helpers.formatNumber}}{{to}}{{/helpers.formatNumber}}
- {{/to}}
- `,
- footer: '',
-};
diff --git a/src/widgets/price-ranges/price-ranges.js b/src/widgets/price-ranges/price-ranges.js
deleted file mode 100644
index c026af46d5..0000000000
--- a/src/widgets/price-ranges/price-ranges.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import PriceRanges from '../../components/PriceRanges/PriceRanges.js';
-import connectPriceRanges from '../../connectors/price-ranges/connectPriceRanges.js';
-import defaultTemplates from './defaultTemplates.js';
-
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-price-ranges');
-
-const renderer = ({
- containerNode,
- templates,
- renderState,
- collapsible,
- cssClasses,
- labels,
- currency,
- autoHideContainer,
-}) => ({ refine, items, instantSearchInstance }, isFirstRendering) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- const shouldAutoHideContainer = autoHideContainer && items.length === 0;
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-priceRanges({
- container,
- attributeName,
- [ currency=$ ],
- [ cssClasses.{root,header,body,list,item,active,link,form,label,input,currency,separator,button,footer} ],
- [ templates.{header,item,footer} ],
- [ labels.{currency,separator,button} ],
- [ autoHideContainer=true ],
- [ collapsible=false ]
-})`;
-
-/**
- * @typedef {Object} PriceRangeClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [list] CSS class to add to the wrapping list element.
- * @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [active] CSS class to add to the active item element.
- * @property {string|string[]} [link] CSS class to add to each link element.
- * @property {string|string[]} [form] CSS class to add to the form element.
- * @property {string|string[]} [label] CSS class to add to each wrapping label of the form.
- * @property {string|string[]} [input] CSS class to add to each input of the form.
- * @property {string|string[]} [currency] CSS class to add to each currency element of the form.
- * @property {string|string[]} [separator] CSS class to add to the separator of the form.
- * @property {string|string[]} [button] CSS class to add to the submit button of the form.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- */
-
-/**
- * @typedef {Object} PriceRangeLabels
- * @property {string} [separator] Separator label, between min and max.
- * @property {string} [button] Button label.
- */
-
-/**
- * @typedef {Object} PriceRangeTemplates
- * @property {string|function({from: number, to: number, currency: string})} [item] Item template. Template data: `from`, `to` and `currency`
- */
-
-/**
- * @typedef {Object} PriceRangeWidgetOptions
- * @property {string|HTMLElement} container Valid CSS Selector as a string or DOMElement.
- * @property {string} attributeName Name of the attribute for faceting.
- * @property {PriceRangeTemplates} [templates] Templates to use for the widget.
- * @property {string} [currency='$'] The currency to display.
- * @property {PriceRangeLabels} [labels] Labels to use for the widget.
- * @property {boolean} [autoHideContainer=true] Hide the container when no refinements available.
- * @property {PriceRangeClasses} [cssClasses] CSS classes to add.
- * @property {boolean|{collapsed: boolean}} [collapsible=false] Hide the widget body and footer when clicking on header.
- */
-
-/**
- * Price ranges widget lets the user choose from of a set of predefined ranges. The ranges are
- * displayed in a list.
- *
- * @requirements
- * The attribute passed to `attributeName` must be declared as an
- * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
- * in your Algolia settings.
- *
- * The values inside this attribute must be JavaScript numbers (not strings).
- * @type {WidgetFactory}
- * @devNovel PriceRanges
- * @category filter
- * @param {PriceRangeWidgetOptions} $0 The PriceRanges widget options.
- * @return {Widget} A new instance of PriceRanges widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.priceRanges({
- * container: '#price-ranges',
- * attributeName: 'price',
- * labels: {
- * currency: '$',
- * separator: 'to',
- * button: 'Go'
- * },
- * templates: {
- * header: 'Price'
- * }
- * })
- * );
- */
-export default function priceRanges({
- container,
- attributeName,
- cssClasses: userCssClasses = {},
- templates = defaultTemplates,
- collapsible = false,
- labels: userLabels = {},
- currency: userCurrency = '$',
- autoHideContainer = true,
-} = {}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const labels = {
- button: 'Go',
- separator: 'to',
- ...userLabels,
- };
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- list: cx(bem('list'), userCssClasses.list),
- link: cx(bem('link'), userCssClasses.link),
- item: cx(bem('item'), userCssClasses.item),
- active: cx(bem('item', 'active'), userCssClasses.active),
- form: cx(bem('form'), userCssClasses.form),
- label: cx(bem('label'), userCssClasses.label),
- input: cx(bem('input'), userCssClasses.input),
- currency: cx(bem('currency'), userCssClasses.currency),
- button: cx(bem('button'), userCssClasses.button),
- separator: cx(bem('separator'), userCssClasses.separator),
- footer: cx(bem('footer'), userCssClasses.footer),
- };
-
- // before we had opts.currency, you had to pass labels.currency
- const currency =
- userLabels.currency !== undefined ? userLabels.currency : userCurrency;
-
- const specializedRenderer = renderer({
- containerNode,
- templates,
- renderState: {},
- collapsible,
- cssClasses,
- labels,
- currency,
- autoHideContainer,
- });
-
- try {
- const makeWidget = connectPriceRanges(specializedRenderer, () =>
- unmountComponentAtNode(containerNode)
- );
- return makeWidget({ attributeName });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/range-input/__tests__/__snapshots__/range-input-test.js.snap b/src/widgets/range-input/__tests__/__snapshots__/range-input-test.js.snap
index 28c3860719..29400b84b0 100644
--- a/src/widgets/range-input/__tests__/__snapshots__/range-input-test.js.snap
+++ b/src/widgets/range-input/__tests__/__snapshots__/range-input-test.js.snap
@@ -1,46 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rangeInput expect to render with custom classNames 1`] = `
-
`;
-exports[`rangeInput expect to render with custom labels 1`] = `
- {
});
describe('rangeInput', () => {
- const attributeName = 'aNumAttr';
+ const attribute = 'aNumAttr';
const createContainer = () => document.createElement('div');
- const instantSearchInstance = { templatesConfig: undefined };
+ const instantSearchInstance = {};
const createHelper = () =>
new AlgoliasearchHelper(
{
@@ -22,7 +22,7 @@ describe('rangeInput', () => {
},
},
'indexName',
- { disjunctiveFacets: [attributeName] }
+ { disjunctiveFacets: [attribute] }
);
afterEach(() => {
@@ -35,7 +35,7 @@ describe('rangeInput', () => {
const results = {
disjunctiveFacets: [
{
- name: attributeName,
+ name: attribute,
stats: {
min: 10,
max: 500,
@@ -46,7 +46,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
});
widget.init({ helper, instantSearchInstance });
@@ -65,7 +65,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
});
widget.init({ helper, instantSearchInstance });
@@ -82,20 +82,17 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
cssClasses: {
- root: 'custom-root',
- header: 'custom-header',
- body: 'custom-body',
- form: 'custom-form',
- fieldset: 'custom-fieldset',
- labelMin: 'custom-labelMin',
- inputMin: 'custom-inputMin',
- separator: 'custom-separator',
- labelMax: 'custom-labelMax',
- inputMax: 'custom-inputMax',
- submit: 'custom-submit',
- footer: 'custom-footer',
+ root: 'root',
+ noRefinement: 'noRefinement',
+ form: 'form',
+ label: 'label',
+ input: 'input',
+ inputMin: 'inputMin',
+ inputMax: 'inputMax',
+ separator: 'separator',
+ submit: 'submit',
},
});
@@ -106,17 +103,17 @@ describe('rangeInput', () => {
expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
- it('expect to render with custom labels', () => {
+ it('expect to render with custom templates', () => {
const container = createContainer();
const helper = createHelper();
const results = [];
const widget = rangeInput({
container,
- attributeName,
- labels: {
- separator: 'custom-separator',
- submit: 'custom-submit',
+ attribute,
+ templates: {
+ separatorText: 'custom separator',
+ submitText: 'custom submit',
},
});
@@ -134,7 +131,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
min: 20,
});
@@ -152,7 +149,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
max: 480,
});
@@ -169,7 +166,7 @@ describe('rangeInput', () => {
const results = {
disjunctiveFacets: [
{
- name: attributeName,
+ name: attribute,
stats: {
min: 10,
max: 500,
@@ -178,12 +175,12 @@ describe('rangeInput', () => {
],
};
- helper.addNumericRefinement(attributeName, '>=', 25);
- helper.addNumericRefinement(attributeName, '<=', 475);
+ helper.addNumericRefinement(attribute, '>=', 25);
+ helper.addNumericRefinement(attribute, '<=', 475);
const widget = rangeInput({
container,
- attributeName,
+ attribute,
});
widget.init({ helper, instantSearchInstance });
@@ -202,12 +199,12 @@ describe('rangeInput', () => {
const helper = createHelper();
const results = {};
- helper.addNumericRefinement(attributeName, '>=', 10);
- helper.addNumericRefinement(attributeName, '<=', 500);
+ helper.addNumericRefinement(attribute, '>=', 10);
+ helper.addNumericRefinement(attribute, '<=', 500);
const widget = rangeInput({
container,
- attributeName,
+ attribute,
min: 10,
max: 500,
});
@@ -223,26 +220,6 @@ describe('rangeInput', () => {
});
});
- it('expect to render hidden', () => {
- const container = createContainer();
- const helper = createHelper();
- const results = [];
-
- const widget = rangeInput({
- container,
- attributeName,
- min: 20,
- max: 20,
- });
-
- widget.init({ helper, instantSearchInstance });
- widget.render({ results, helper });
-
- expect(ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer).toBe(
- true
- );
- });
-
it('expect to call refine', () => {
const container = createContainer();
const helper = createHelper();
@@ -251,7 +228,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
});
// Override _refine behavior to be able to check
@@ -274,7 +251,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
precision: 2,
});
@@ -292,7 +269,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
precision: 0,
});
@@ -310,7 +287,7 @@ describe('rangeInput', () => {
const widget = rangeInput({
container,
- attributeName,
+ attribute,
precision: 1,
});
@@ -324,10 +301,10 @@ describe('rangeInput', () => {
describe('throws', () => {
it('throws an exception when no container', () => {
- expect(() => rangeInput({ attributeName: '' })).toThrow(/^Usage:/);
+ expect(() => rangeInput({ attribute: '' })).toThrow(/^Usage:/);
});
- it('throws an exception when no attributeName', () => {
+ it('throws an exception when no attribute', () => {
expect(() =>
rangeInput({
container: document.createElement('div'),
diff --git a/src/widgets/range-input/defaultTemplates.js b/src/widgets/range-input/defaultTemplates.js
deleted file mode 100644
index 05269f4007..0000000000
--- a/src/widgets/range-input/defaultTemplates.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- header: '',
- footer: '',
-};
diff --git a/src/widgets/range-input/range-input.js b/src/widgets/range-input/range-input.js
index 7cdf1b067a..1fb00e5afb 100644
--- a/src/widgets/range-input/range-input.js
+++ b/src/widgets/range-input/range-input.js
@@ -2,34 +2,20 @@ import React, { render } from 'preact-compat';
import cx from 'classnames';
import RangeInput from '../../components/RangeInput/RangeInput.js';
import connectRange from '../../connectors/range/connectRange.js';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-import defaultTemplates from './defaultTemplates.js';
-
-const bem = bemHelper('ais-range-input');
-
-const renderer = ({
- containerNode,
- templates,
- cssClasses,
- labels,
- autoHideContainer,
- collapsible,
- renderState,
-}) => (
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
+
+const suit = component('RangeInput');
+
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
{ refine, range, start, widgetParams, instantSearchInstance },
isFirstRendering
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
});
-
return;
}
@@ -37,7 +23,6 @@ const renderer = ({
const [minValue, maxValue] = start;
const step = 1 / Math.pow(10, widgetParams.precision);
- const shouldAutoHideContainer = autoHideContainer && rangeMin === rangeMax;
const values = {
min: minValue !== -Infinity && minValue !== rangeMin ? minValue : undefined,
@@ -51,10 +36,7 @@ const renderer = ({
step={step}
values={values}
cssClasses={cssClasses}
- labels={labels}
refine={refine}
- shouldAutoHideContainer={shouldAutoHideContainer}
- collapsible={collapsible}
templateProps={renderState.templateProps}
/>,
containerNode
@@ -64,64 +46,49 @@ const renderer = ({
const usage = `Usage:
rangeInput({
container,
- attributeName,
+ attribute,
[ min ],
[ max ],
[ precision = 0 ],
- [ cssClasses.{root, header, body, form, fieldset, labelMin, inputMin, separator, labelMax, inputMax, submit, footer} ],
- [ templates.{header, footer} ],
- [ labels.{separator, submit} ],
- [ autoHideContainer=true ],
- [ collapsible=false ]
+ [ templates.{separatorText, submitText} ],
+ [ cssClasses.{root, noRefinement, form, label, input, inputMin, inputMax, separator, submit} ],
})`;
/**
* @typedef {Object} RangeInputClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
+ * @property {string|string[]} [noRefinement] CSS class to add to the root element when there's no refinements.
* @property {string|string[]} [form] CSS class to add to the form element.
- * @property {string|string[]} [fieldset] CSS class to add to the fieldset element.
- * @property {string|string[]} [labelMin] CSS class to add to the min label element.
+ * @property {string|string[]} [label] CSS class to add to the label element.
+ * @property {string|string[]} [input] CSS class to add to the input element.
* @property {string|string[]} [inputMin] CSS class to add to the min input element.
- * @property {string|string[]} [separator] CSS class to add to the separator of the form.
- * @property {string|string[]} [labelMax] CSS class to add to the max label element.
* @property {string|string[]} [inputMax] CSS class to add to the max input element.
+ * @property {string|string[]} [separator] CSS class to add to the separator of the form.
* @property {string|string[]} [submit] CSS class to add to the submit button of the form.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
*/
/**
* @typedef {Object} RangeInputTemplates
- * @property {string|function} [header=""] Header template.
- * @property {string|function} [footer=""] Footer template.
- */
-
-/**
- * @typedef {Object} RangeInputLabels
- * @property {string} [separator="to"] Separator label, between min and max.
- * @property {string} [submit="Go"] Button label.
+ * @property {string} [separatorText = "to"] The label of the separator, between min and max.
+ * @property {string} [submitText = "Go"] The label of the submit button.
*/
/**
* @typedef {Object} RangeInputWidgetOptions
* @property {string|HTMLElement} container Valid CSS Selector as a string or DOMElement.
- * @property {string} attributeName Name of the attribute for faceting.
+ * @property {string} attribute Name of the attribute for faceting.
* @property {number} [min] Minimal slider value, default to automatically computed from the result set.
* @property {number} [max] Maximal slider value, defaults to automatically computed from the result set.
* @property {number} [precision = 0] Number of digits after decimal point to use.
+ * @property {RangeInputTemplates} [templates] Labels to use for the widget.
* @property {RangeInputClasses} [cssClasses] CSS classes to add.
- * @property {RangeInputTemplates} [templates] Templates to use for the widget.
- * @property {RangeInputLabels} [labels] Labels to use for the widget.
- * @property {boolean} [autoHideContainer=true] Hide the container when no refinements available.
- * @property {boolean} [collapsible=false] Hide the widget body and footer when clicking on header.
*/
/**
* The range input widget allows a user to select a numeric range using a minimum and maximum input.
*
* @requirements
- * The attribute passed to `attributeName` must be declared as an
+ * The attribute passed to `attribute` must be declared as an
* [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
* in your Algolia settings.
*
@@ -135,28 +102,22 @@ rangeInput({
* search.addWidget(
* instantsearch.widgets.rangeInput({
* container: '#range-input',
- * attributeName: 'price',
- * labels: {
- * separator: 'to',
- * submit: 'Go'
- * },
+ * attribute: 'price',
* templates: {
- * header: 'Price'
- * }
+ * separatorText: 'to',
+ * submitText: 'Go'
+ * },
* })
* );
*/
export default function rangeInput({
container,
- attributeName,
+ attribute,
min,
max,
precision = 0,
cssClasses: userCssClasses = {},
- templates = defaultTemplates,
- labels: userLabels = {},
- autoHideContainer = true,
- collapsible = false,
+ templates: userTemplates = {},
} = {}) {
if (!container) {
throw new Error(usage);
@@ -164,34 +125,37 @@ export default function rangeInput({
const containerNode = getContainerNode(container);
- const labels = {
- separator: 'to',
- submit: 'Go',
- ...userLabels,
+ const templates = {
+ separatorText: 'to',
+ submitText: 'Go',
+ ...userTemplates,
};
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- form: cx(bem('form'), userCssClasses.form),
- fieldset: cx(bem('fieldset'), userCssClasses.fieldset),
- labelMin: cx(bem('labelMin'), userCssClasses.labelMin),
- inputMin: cx(bem('inputMin'), userCssClasses.inputMin),
- separator: cx(bem('separator'), userCssClasses.separator),
- labelMax: cx(bem('labelMax'), userCssClasses.labelMax),
- inputMax: cx(bem('inputMax'), userCssClasses.inputMax),
- submit: cx(bem('submit'), userCssClasses.submit),
- footer: cx(bem('footer'), userCssClasses.footer),
+ root: cx(suit(), userCssClasses.root),
+ noRefinement: cx(suit({ modifierName: 'noRefinement' })),
+ form: cx(suit({ descendantName: 'form' }), userCssClasses.form),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ input: cx(suit({ descendantName: 'input' }), userCssClasses.input),
+ inputMin: cx(
+ suit({ descendantName: 'input', modifierName: 'min' }),
+ userCssClasses.inputMin
+ ),
+ inputMax: cx(
+ suit({ descendantName: 'input', modifierName: 'max' }),
+ userCssClasses.inputMax
+ ),
+ separator: cx(
+ suit({ descendantName: 'separator' }),
+ userCssClasses.separator
+ ),
+ submit: cx(suit({ descendantName: 'submit' }), userCssClasses.submit),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
templates,
- labels,
- autoHideContainer,
- collapsible,
renderState: {},
});
@@ -199,12 +163,12 @@ export default function rangeInput({
const makeWidget = connectRange(specializedRenderer);
return makeWidget({
- attributeName,
+ attribute,
min,
max,
precision,
});
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/range-slider/__tests__/__snapshots__/range-slider-test.js.snap b/src/widgets/range-slider/__tests__/__snapshots__/range-slider-test.js.snap
index c2cd50526d..ea51d5df8f 100644
--- a/src/widgets/range-slider/__tests__/__snapshots__/range-slider-test.js.snap
+++ b/src/widgets/range-slider/__tests__/__snapshots__/range-slider-test.js.snap
@@ -1,35 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rangeSlider widget usage max option will use the results min when only max is passed 1`] = `
-
`;
-exports[`rangeSlider widget usage should \`collapse\` when options is provided 1`] = `
-
-`;
-
-exports[`rangeSlider widget usage should \`shouldAutoHideContainer\` when range min === max 1`] = `
-
-`;
-
exports[`rangeSlider widget usage should render without results 1`] = `
- {
it('throws an exception when no container', () => {
- const attributeName = '';
- expect(() => rangeSlider({ attributeName })).toThrow(/^Usage:/);
+ const attribute = '';
+ expect(() =>
+ rangeSlider({ attribute, step: 1, cssClasses: { root: '' } })
+ ).toThrow(/^Usage:/);
});
- it('throws an exception when no attributeName', () => {
+ it('throws an exception when no attribute', () => {
const container = document.createElement('div');
- expect(() => rangeSlider({ container })).toThrow(/^Usage:/);
+ expect(() =>
+ rangeSlider({
+ container,
+ step: 1,
+ cssClasses: { root: '' },
+ })
+ ).toThrow(/^Usage:/);
});
describe('widget usage', () => {
- const attributeName = 'aNumAttr';
+ const attribute = 'aNumAttr';
let ReactDOM;
let container;
@@ -41,15 +49,14 @@ describe('rangeSlider', () => {
afterEach(() => {
rangeSlider.__ResetDependency__('render');
- rangeSlider.__ResetDependency__('autoHideContainerHOC');
- rangeSlider.__ResetDependency__('headerFooterHOC');
});
it('should render without results', () => {
widget = rangeSlider({
container,
- attributeName,
+ attribute,
cssClasses: { root: ['root', 'cx'] },
+ step: 1,
});
widget.init({ helper, instantSearchInstance });
@@ -59,86 +66,51 @@ describe('rangeSlider', () => {
expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
- it('should `shouldAutoHideContainer` when range min === max', () => {
- const results = {
- disjunctiveFacets: [
- {
- name: attributeName,
- stats: {
- min: 65,
- max: 65,
- },
- },
- ],
- };
-
- widget = rangeSlider({
- container,
- attributeName,
- cssClasses: { root: ['root', 'cx'] },
- });
-
- widget.init({ helper, instantSearchInstance });
- widget.render({ results, helper });
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(
- ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer
- ).toEqual(true);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
- it('should `collapse` when options is provided', () => {
- const results = {};
-
- widget = rangeSlider({
- container,
- attributeName,
- collapsible: {
- collapsed: true,
- },
- });
-
- widget.init({ helper, instantSearchInstance });
- widget.render({ results, helper });
-
- expect(ReactDOM.render).toHaveBeenCalledTimes(1);
- expect(
- ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer
- ).toEqual(true);
- expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
- });
-
describe('min option', () => {
it('refines when no previous configuration', () => {
- widget = rangeSlider({ container, attributeName, min: 100 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ min: 100,
+ step: 1,
+ cssClasses: { root: '' },
+ });
expect(widget.getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
- numericRefinements: { [attributeName]: { '>=': [100] } },
+ disjunctiveFacets: [attribute],
+ numericRefinements: { [attribute]: { '>=': [100] } },
});
});
it('does not refine when previous configuration', () => {
widget = rangeSlider({
container,
- attributeName: 'aNumAttr',
+ attribute: 'aNumAttr',
min: 100,
+ step: 1,
+ cssClasses: { root: '' },
});
expect(
widget.getConfiguration({
- numericRefinements: { [attributeName]: {} },
+ numericRefinements: { [attribute]: {} },
})
).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
});
it('works along with max option', () => {
- widget = rangeSlider({ container, attributeName, min: 100, max: 200 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ min: 100,
+ max: 200,
+ step: 1,
+ cssClasses: { root: '' },
+ });
expect(widget.getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
numericRefinements: {
- [attributeName]: {
+ [attribute]: {
'>=': [100],
'<=': [200],
},
@@ -147,7 +119,14 @@ describe('rangeSlider', () => {
});
it('sets the right range', () => {
- widget = rangeSlider({ container, attributeName, min: 100, max: 200 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ min: 100,
+ max: 200,
+ step: 1,
+ cssClasses: { root: '' },
+ });
helper.setState(widget.getConfiguration());
widget.init({ helper, instantSearchInstance });
widget.render({ results: {}, helper });
@@ -160,7 +139,7 @@ describe('rangeSlider', () => {
const results = {
disjunctiveFacets: [
{
- name: attributeName,
+ name: attribute,
stats: {
min: 1.99,
max: 4999.98,
@@ -169,7 +148,13 @@ describe('rangeSlider', () => {
],
};
- widget = rangeSlider({ container, attributeName, min: 100 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ min: 100,
+ step: 1,
+ cssClasses: { root: '' },
+ });
helper.setState(widget.getConfiguration());
widget.init({ helper, instantSearchInstance });
widget.render({ results, helper });
@@ -182,21 +167,33 @@ describe('rangeSlider', () => {
describe('max option', () => {
it('refines when no previous configuration', () => {
- widget = rangeSlider({ container, attributeName, max: 100 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ max: 100,
+ step: 1,
+ cssClasses: { root: '' },
+ });
expect(widget.getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
- numericRefinements: { [attributeName]: { '<=': [100] } },
+ disjunctiveFacets: [attribute],
+ numericRefinements: { [attribute]: { '<=': [100] } },
});
});
it('does not refine when previous configuration', () => {
- widget = rangeSlider({ container, attributeName, max: 100 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ max: 100,
+ step: 1,
+ cssClasses: { root: '' },
+ });
expect(
widget.getConfiguration({
- numericRefinements: { [attributeName]: {} },
+ numericRefinements: { [attribute]: {} },
})
).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
});
@@ -204,7 +201,7 @@ describe('rangeSlider', () => {
const results = {
disjunctiveFacets: [
{
- name: attributeName,
+ name: attribute,
stats: {
min: 1.99,
max: 4999.98,
@@ -213,7 +210,13 @@ describe('rangeSlider', () => {
],
};
- widget = rangeSlider({ container, attributeName, max: 100 });
+ widget = rangeSlider({
+ container,
+ attribute,
+ max: 100,
+ step: 1,
+ cssClasses: { root: '' },
+ });
helper.setState(widget.getConfiguration());
widget.init({ helper, instantSearchInstance });
widget.render({ results, helper });
@@ -228,13 +231,18 @@ describe('rangeSlider', () => {
let results;
beforeEach(() => {
- widget = rangeSlider({ container, attributeName });
+ widget = rangeSlider({
+ container,
+ attribute,
+ step: 1,
+ cssClasses: { root: '' },
+ });
widget.init({ helper, instantSearchInstance });
results = {
disjunctiveFacets: [
{
- name: attributeName,
+ name: attribute,
stats: {
min: 1.99,
max: 4999.98,
@@ -248,7 +256,7 @@ describe('rangeSlider', () => {
it('configures the disjunctiveFacets', () => {
expect(widget.getConfiguration()).toEqual({
- disjunctiveFacets: [attributeName],
+ disjunctiveFacets: [attribute],
});
});
@@ -279,9 +287,7 @@ describe('rangeSlider', () => {
const state1 = helper.state;
expect(helper.search).toHaveBeenCalledTimes(1);
- expect(state1).toEqual(
- state0.addNumericRefinement(attributeName, '>=', 3)
- );
+ expect(state1).toEqual(state0.addNumericRefinement(attribute, '>=', 3));
});
it('calls the refinement function if refined with max-1', () => {
@@ -294,7 +300,7 @@ describe('rangeSlider', () => {
expect(helper.search).toHaveBeenCalledTimes(1);
expect(state1).toEqual(
- state0.addNumericRefinement(attributeName, '<=', 4999)
+ state0.addNumericRefinement(attribute, '<=', 4999)
);
});
@@ -309,21 +315,23 @@ describe('rangeSlider', () => {
expect(helper.search).toHaveBeenCalledTimes(1);
expect(state1).toEqual(
state0
- .addNumericRefinement(attributeName, '>=', 3)
- .addNumericRefinement(attributeName, '<=', 4999)
+ .addNumericRefinement(attribute, '>=', 3)
+ .addNumericRefinement(attribute, '<=', 4999)
);
});
it("expect to clamp the min value to the max range when it's greater than range", () => {
widget = rangeSlider({
container,
- attributeName,
+ attribute,
+ step: 1,
+ cssClasses: { root: '' },
});
widget.init({ helper, instantSearchInstance });
- helper.addNumericRefinement(attributeName, '>=', 5550);
- helper.addNumericRefinement(attributeName, '<=', 6000);
+ helper.addNumericRefinement(attribute, '>=', 5550);
+ helper.addNumericRefinement(attribute, '<=', 6000);
widget.render({ results, helper });
@@ -333,13 +341,15 @@ describe('rangeSlider', () => {
it("expect to clamp the max value to the min range when it's lower than range", () => {
widget = rangeSlider({
container,
- attributeName,
+ attribute,
+ step: 1,
+ cssClasses: { root: '' },
});
widget.init({ helper, instantSearchInstance });
- helper.addNumericRefinement(attributeName, '>=', -50);
- helper.addNumericRefinement(attributeName, '<=', 0);
+ helper.addNumericRefinement(attribute, '>=', -50);
+ helper.addNumericRefinement(attribute, '<=', 0);
widget.render({ results, helper });
diff --git a/src/widgets/range-slider/range-slider.js b/src/widgets/range-slider/range-slider.js
index 0fd9a37d42..13a68b2c27 100644
--- a/src/widgets/range-slider/range-slider.js
+++ b/src/widgets/range-slider/range-slider.js
@@ -1,44 +1,22 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
import Slider from '../../components/Slider/Slider.js';
import connectRange from '../../connectors/range/connectRange.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const defaultTemplates = {
- header: '',
- footer: '',
-};
-
-const bem = bemHelper('ais-range-slider');
+const suit = component('RangeSlider');
-const renderer = ({
- containerNode,
- cssClasses,
- pips,
- step,
- tooltips,
- autoHideContainer,
- collapsible,
- renderState,
- templates,
-}) => ({ refine, range, start, instantSearchInstance }, isFirstRendering) => {
+const renderer = ({ containerNode, cssClasses, pips, step, tooltips }) => (
+ { refine, range, start },
+ isFirstRendering
+) => {
if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
+ // There's no information at this point, let's render nothing.
return;
}
const { min: minRange, max: maxRange } = range;
- const shouldAutoHideContainer = autoHideContainer && minRange === maxRange;
const [minStart, maxStart] = start;
const minFinite = minStart === -Infinity ? minRange : minStart;
@@ -62,9 +40,6 @@ const renderer = ({
tooltips={tooltips}
step={step}
pips={pips}
- shouldAutoHideContainer={shouldAutoHideContainer}
- collapsible={collapsible}
- templateProps={renderState.templateProps}
/>,
containerNode
);
@@ -73,32 +48,21 @@ const renderer = ({
const usage = `Usage:
rangeSlider({
container,
- attributeName,
+ attribute,
[ min ],
[ max ],
[ pips = true ],
[ step = 1 ],
[ precision = 0 ],
- [ tooltips=true ],
- [ templates.{header, footer} ],
- [ cssClasses.{root, header, body, footer} ],
- [ autoHideContainer=true ],
- [ collapsible=false ],
+ [ tooltips = true ],
+ [ cssClasses.{root, disabledRoot} ]
});
`;
-/**
- * @typedef {Object} RangeSliderTemplates
- * @property {string|function} [header=""] Header template.
- * @property {string|function} [footer=""] Footer template.
- */
-
/**
* @typedef {Object} RangeSliderCssClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
+ * @property {string|string[]} [disabledRoot] CSS class to add to the disabled root element.
*/
/**
@@ -108,25 +72,17 @@ rangeSlider({
* `format: function(rawValue) {return '$' + Math.round(rawValue).toLocaleString()}`
*/
-/**
- * @typedef {Object} RangeSliderCollapsibleOptions
- * @property {boolean} [collapsed] Initially collapsed state of a collapsible widget.
- */
-
/**
* @typedef {Object} RangeSliderWidgetOptions
* @property {string|HTMLElement} container CSS Selector or DOMElement to insert the widget.
- * @property {string} attributeName Name of the attribute for faceting.
+ * @property {string} attribute Name of the attribute for faceting.
* @property {boolean|RangeSliderTooltipOptions} [tooltips=true] Should we show tooltips or not.
* The default tooltip will show the raw value.
* You can also provide an object with a format function as an attribute.
* So that you can format the tooltip display value as you want
- * @property {RangeSliderTemplates} [templates] Templates to use for the widget.
- * @property {boolean} [autoHideContainer=true] Hide the container when no refinements available.
* @property {RangeSliderCssClasses} [cssClasses] CSS classes to add to the wrapping elements.
* @property {boolean} [pips=true] Show slider pips.
* @property {number} [precision = 0] Number of digits after decimal point to use.
- * @property {boolean|RangeSliderCollapsibleOptions} [collapsible=false] Hide the widget body and footer when clicking on header.
* @property {number} [step] Every handle move will jump that number of steps.
* @property {number} [min] Minimal slider value, default to automatically computed from the result set.
* @property {number} [max] Maximal slider value, defaults to automatically computed from the result set.
@@ -137,7 +93,7 @@ rangeSlider({
* results based on a single numeric range.
*
* @requirements
- * The attribute passed to `attributeName` must be declared as an
+ * The attribute passed to `attribute` must be declared as an
* [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
* in your Algolia settings.
*
@@ -152,10 +108,7 @@ rangeSlider({
* search.addWidget(
* instantsearch.widgets.rangeSlider({
* container: '#price',
- * attributeName: 'price',
- * templates: {
- * header: 'Price'
- * },
+ * attribute: 'price',
* tooltips: {
* format: function(rawValue) {
* return '$' + Math.round(rawValue).toLocaleString();
@@ -166,17 +119,14 @@ rangeSlider({
*/
export default function rangeSlider({
container,
- attributeName,
+ attribute,
min,
max,
- templates = defaultTemplates,
cssClasses: userCssClasses = {},
step,
pips = true,
precision = 0,
tooltips = true,
- autoHideContainer = true,
- collapsible = false,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -184,10 +134,11 @@ export default function rangeSlider({
const containerNode = getContainerNode(container);
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
+ root: cx(suit(), userCssClasses.root),
+ disabledRoot: cx(
+ suit({ modifierName: 'disabled' }),
+ userCssClasses.disabledRoot
+ ),
};
const specializedRenderer = renderer({
@@ -196,9 +147,6 @@ export default function rangeSlider({
pips,
tooltips,
renderState: {},
- templates,
- autoHideContainer,
- collapsible,
cssClasses,
});
@@ -206,8 +154,8 @@ export default function rangeSlider({
const makeWidget = connectRange(specializedRenderer, () =>
unmountComponentAtNode(containerNode)
);
- return makeWidget({ attributeName, min, max, precision });
- } catch (e) {
+ return makeWidget({ attribute, min, max, precision });
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/rating-menu/__tests__/__snapshots__/rating-menu-test.js.snap b/src/widgets/rating-menu/__tests__/__snapshots__/rating-menu-test.js.snap
new file mode 100644
index 0000000000..c69725592a
--- /dev/null
+++ b/src/widgets/rating-menu/__tests__/__snapshots__/rating-menu-test.js.snap
@@ -0,0 +1,239 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ratingMenu() calls twice ReactDOM.render( , container) 1`] = `
+{{/count}}{{^count}}{{/count}}
+ {{#stars}}
+ {{#.}} {{/.}}{{^.}} {{/.}}
+ {{/stars}}
+ & Up
+ {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
+{{#count}}{{/count}}{{^count}}
{{/count}}",
+ },
+ "templatesConfig": undefined,
+ "useCustomCompileOptions": Object {
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+>
+
+
+
+
+
+`;
+
+exports[`ratingMenu() calls twice ReactDOM.render( , container) 2`] = `
+{{/count}}{{^count}}{{/count}}
+ {{#stars}}
+ {{#.}} {{/.}}{{^.}} {{/.}}
+ {{/stars}}
+ & Up
+ {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
+{{#count}}{{/count}}{{^count}}
{{/count}}",
+ },
+ "templatesConfig": undefined,
+ "useCustomCompileOptions": Object {
+ "item": false,
+ },
+ }
+ }
+ toggleRefinement={[Function]}
+>
+
+
+
+
+
+`;
diff --git a/src/widgets/rating-menu/__tests__/rating-menu-test.js b/src/widgets/rating-menu/__tests__/rating-menu-test.js
new file mode 100644
index 0000000000..743267525a
--- /dev/null
+++ b/src/widgets/rating-menu/__tests__/rating-menu-test.js
@@ -0,0 +1,196 @@
+import jsHelper, { SearchResults } from 'algoliasearch-helper';
+import ratingMenu from '../rating-menu.js';
+
+describe('ratingMenu()', () => {
+ const attribute = 'anAttrName';
+ let ReactDOM;
+ let container;
+ let widget;
+ let helper;
+ let state;
+ let createURL;
+ let results;
+
+ beforeEach(() => {
+ ReactDOM = { render: jest.fn() };
+ ratingMenu.__Rewire__('render', ReactDOM.render);
+
+ container = document.createElement('div');
+ widget = ratingMenu({
+ container,
+ attribute,
+ cssClasses: { body: ['body', 'cx'] },
+ });
+ helper = jsHelper({}, '', widget.getConfiguration({}));
+ jest.spyOn(helper, 'clearRefinements');
+ jest.spyOn(helper, 'addDisjunctiveFacetRefinement');
+ jest.spyOn(helper, 'getRefinements');
+ helper.search = jest.fn();
+
+ state = {
+ toggleRefinement: jest.fn(),
+ };
+ results = {
+ getFacetValues: jest.fn().mockReturnValue([]),
+ hits: [],
+ };
+ createURL = () => '#';
+ widget.init({
+ helper,
+ instantSearchInstance: { templatesConfig: undefined },
+ });
+ });
+
+ it('configures the underlying disjunctive facet', () => {
+ expect(widget.getConfiguration()).toEqual({
+ disjunctiveFacets: ['anAttrName'],
+ });
+ });
+
+ it('calls twice ReactDOM.render( , container)', () => {
+ widget.render({ state, helper, results, createURL });
+ widget.render({ state, helper, results, createURL });
+
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
+ });
+
+ it('hide the count==0 when there is a refinement', () => {
+ helper.addDisjunctiveFacetRefinement(attribute, 1);
+ const _results = new SearchResults(helper.state, [
+ {
+ facets: {
+ [attribute]: { 1: 42 },
+ },
+ },
+ {},
+ ]);
+
+ widget.render({ state, helper, results: _results, createURL });
+ expect(ReactDOM.render).toHaveBeenCalledTimes(1);
+ expect(ReactDOM.render.mock.calls[0][0].props.facetValues).toEqual([
+ {
+ count: 42,
+ isRefined: true,
+ name: '1',
+ value: '1',
+ stars: [true, false, false, false, false],
+ },
+ ]);
+ });
+
+ it("doesn't call the refinement functions if not refined", () => {
+ helper.getRefinements = jest.fn().mockReturnValue([]);
+ widget.render({ state, helper, results, createURL });
+ expect(helper.clearRefinements).toHaveBeenCalledTimes(0);
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledTimes(0);
+ expect(helper.search).toHaveBeenCalledTimes(0);
+ });
+
+ it('refines the search', () => {
+ helper.getRefinements = jest.fn().mockReturnValue([]);
+ widget._toggleRefinement('3');
+ expect(helper.clearRefinements).toHaveBeenCalledTimes(1);
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledTimes(3);
+ expect(helper.search).toHaveBeenCalledTimes(1);
+ });
+
+ it('toggles the refinements', () => {
+ helper.addDisjunctiveFacetRefinement(attribute, 2);
+ helper.addDisjunctiveFacetRefinement.mockReset();
+ widget._toggleRefinement('2');
+ expect(helper.clearRefinements).toHaveBeenCalledTimes(1);
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledTimes(0);
+ expect(helper.search).toHaveBeenCalledTimes(1);
+ });
+
+ it('toggles the refinements with another facet', () => {
+ helper.getRefinements = jest.fn().mockReturnValue([{ value: '2' }]);
+ widget._toggleRefinement('4');
+ expect(helper.clearRefinements).toHaveBeenCalledTimes(1);
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledTimes(2);
+ expect(helper.search).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return the right facet counts and results', () => {
+ const _widget = ratingMenu({
+ container,
+ attribute,
+ cssClasses: { body: ['body', 'cx'] },
+ });
+ const _helper = jsHelper({}, '', _widget.getConfiguration({}));
+ _helper.search = jest.fn();
+
+ _widget.init({
+ helper: _helper,
+ state: _helper.state,
+ createURL: () => '#',
+ onHistoryChange: () => {},
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+
+ _widget.render({
+ results: new SearchResults(_helper.state, [
+ {
+ facets: {
+ [attribute]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
+ },
+ },
+ {},
+ ]),
+ state: _helper.state,
+ helper: _helper,
+ createURL: () => '#',
+ instantSearchInstance: {
+ templatesConfig: {},
+ },
+ });
+
+ expect(
+ ReactDOM.render.mock.calls[ReactDOM.render.mock.calls.length - 1][0].props
+ .facetValues
+ ).toEqual([
+ {
+ count: 1000,
+ isRefined: false,
+
+ name: '4',
+ value: '4',
+ stars: [true, true, true, true, false],
+ },
+ {
+ count: 1050,
+ isRefined: false,
+
+ name: '3',
+ value: '3',
+ stars: [true, true, true, false, false],
+ },
+ {
+ count: 1070,
+ isRefined: false,
+
+ name: '2',
+ value: '2',
+ stars: [true, true, false, false, false],
+ },
+ {
+ count: 1080,
+ isRefined: false,
+
+ name: '1',
+ value: '1',
+ stars: [true, false, false, false, false],
+ },
+ ]);
+ });
+
+ afterEach(() => {
+ ratingMenu.__ResetDependency__('render');
+ });
+});
diff --git a/src/widgets/rating-menu/defaultTemplates.js b/src/widgets/rating-menu/defaultTemplates.js
new file mode 100644
index 0000000000..a4c5c6a979
--- /dev/null
+++ b/src/widgets/rating-menu/defaultTemplates.js
@@ -0,0 +1,9 @@
+export default {
+ item: `{{#count}}{{/count}}{{^count}}{{/count}}
+ {{#stars}}
+ {{#.}} {{/.}}{{^.}} {{/.}}
+ {{/stars}}
+ & Up
+ {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
+{{#count}}{{/count}}{{^count}}
{{/count}}`,
+};
diff --git a/src/widgets/rating-menu/rating-menu.js b/src/widgets/rating-menu/rating-menu.js
new file mode 100644
index 0000000000..292904fc50
--- /dev/null
+++ b/src/widgets/rating-menu/rating-menu.js
@@ -0,0 +1,172 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import RefinementList from '../../components/RefinementList/RefinementList.js';
+import connectRatingMenu from '../../connectors/rating-menu/connectRatingMenu.js';
+import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('RatingMenu');
+
+const renderer = ({ containerNode, cssClasses, templates, renderState }) => (
+ { refine, items, createURL, instantSearchInstance },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ renderState.templateProps = prepareTemplateProps({
+ defaultTemplates,
+ templatesConfig: instantSearchInstance.templatesConfig,
+ templates,
+ });
+
+ return;
+ }
+
+ render(
+
+
+
+
+
+
+
+
+
+ ,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+ratingMenu({
+ container,
+ attribute,
+ [ max = 5 ],
+ [ cssClasses.{root, list, item, selectedItem, disabledItem, link, starIcon, fullStarIcon, emptyStarIcon, label, count} ],
+ [ templates.{item} ],
+})`;
+
+/**
+ * @typedef {Object} RatingMenuWidgetTemplates
+ * @property {string|function} [item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties.
+ */
+
+/**
+ * @typedef {Object} RatingMenuWidgetCssClasses
+ * @property {string|string[]} [root] CSS class to add to the root element.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element when there's no refinements.
+ * @property {string|string[]} [list] CSS class to add to the list element.
+ * @property {string|string[]} [item] CSS class to add to each item element.
+ * @property {string|string[]} [selectedItem] CSS class to add the selected item element.
+ * @property {string|string[]} [disabledItem] CSS class to add a disabled item element.
+ * @property {string|string[]} [link] CSS class to add to each link element.
+ * @property {string|string[]} [starIcon] CSS class to add to each star element (when using the default template).
+ * @property {string|string[]} [fullStarIcon] CSS class to add to each full star element (when using the default template).
+ * @property {string|string[]} [emptyStarIcon] CSS class to add to each empty star element (when using the default template).
+ * @property {string|string[]} [label] CSS class to add to each label.
+ * @property {string|string[]} [count] CSS class to add to each counter.
+ */
+
+/**
+ * @typedef {Object} RatingMenuWidgetOptions
+ * @property {string|HTMLElement} container Place where to insert the widget in your webpage.
+ * @property {string} attribute Name of the attribute in your records that contains the ratings.
+ * @property {number} [max = 5] The maximum rating value.
+ * @property {RatingMenuWidgetTemplates} [templates] Templates to use for the widget.
+ * @property {RatingMenuWidgetCssClasses} [cssClasses] CSS classes to add.
+ */
+
+/**
+ * Rating menu is used for displaying grade like filters. The values are normalized within boundaries.
+ *
+ * The maximum value can be set (with `max`), the minimum is always 0.
+ *
+ * @requirements
+ * The attribute passed to `attribute` must be declared as an
+ * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
+ * in your Algolia settings.
+ *
+ * The values inside this attribute must be JavaScript numbers (not strings).
+ *
+ * @type {WidgetFactory}
+ * @devNovel RatingMenu
+ * @category filter
+ * @param {RatingMenuWidgetOptions} $0 RatingMenu widget options.
+ * @return {Widget} A new RatingMenu widget instance.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.ratingMenu({
+ * container: '#stars',
+ * attribute: 'rating',
+ * max: 5,
+ * })
+ * );
+ */
+export default function ratingMenu({
+ container,
+ attribute,
+ max = 5,
+ cssClasses: userCssClasses = {},
+ templates = defaultTemplates,
+} = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ disabledItem: cx(
+ suit({ descendantName: 'item', modifierName: 'disabled' }),
+ userCssClasses.disabledItem
+ ),
+ link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
+ starIcon: cx(suit({ descendantName: 'starIcon' }), userCssClasses.starIcon),
+ fullStarIcon: cx(
+ suit({ descendantName: 'starIcon', modifierName: 'full' }),
+ userCssClasses.fullStarIcon
+ ),
+ emptyStarIcon: cx(
+ suit({ descendantName: 'starIcon', modifierName: 'empty' }),
+ userCssClasses.emptyStarIcon
+ ),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ count: cx(suit({ descendantName: 'count' }), userCssClasses.count),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ renderState: {},
+ templates,
+ });
+
+ try {
+ const makeWidget = connectRatingMenu(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeWidget({ attribute, max });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap b/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap
index a25fdd6910..0e58126321 100644
--- a/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap
+++ b/src/widgets/refinement-list/__tests__/__snapshots__/refinement-list-test.js.snap
@@ -1,24 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`refinementList() render renders transformed items correctly 1`] = `
-
- {{{highlighted}}}
+ {{{highlighted}}}
{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
",
+ "searchableNoResults": "No results",
+ "showMoreText": "
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ ",
},
"templatesConfig": Object {},
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
"item": false,
+ "searchableNoResults": false,
+ "showMoreText": false,
},
}
}
diff --git a/src/widgets/refinement-list/__tests__/refinement-list-test.js b/src/widgets/refinement-list/__tests__/refinement-list-test.js
index ec1bb97b68..dfbfb437b6 100644
--- a/src/widgets/refinement-list/__tests__/refinement-list-test.js
+++ b/src/widgets/refinement-list/__tests__/refinement-list-test.js
@@ -1,12 +1,10 @@
-import algoliasearchHelper from 'algoliasearch-helper';
-const SearchParameters = algoliasearchHelper.SearchParameters;
+import { SearchParameters } from 'algoliasearch-helper';
import refinementList from '../refinement-list.js';
+
const instantSearchInstance = { templatesConfig: {} };
describe('refinementList()', () => {
- let autoHideContainer;
let container;
- let headerFooter;
let options;
let widget;
let ReactDOM;
@@ -16,16 +14,12 @@ describe('refinementList()', () => {
ReactDOM = { render: jest.fn() };
refinementList.__Rewire__('render', ReactDOM.render);
- autoHideContainer = jest.fn();
- refinementList.__Rewire__('autoHideContainerHOC', autoHideContainer);
- headerFooter = jest.fn();
- refinementList.__Rewire__('headerFooterHOC', headerFooter);
});
describe('instantiated with wrong parameters', () => {
it('should fail if no container', () => {
// Given
- options = { container: undefined, attributeName: 'foo' };
+ options = { container: undefined, attribute: 'foo' };
// Then
expect(() => {
@@ -48,7 +42,7 @@ describe('refinementList()', () => {
}
beforeEach(() => {
- options = { container, attributeName: 'attributeName' };
+ options = { container, attribute: 'attribute' };
results = {
getFacetValues: jest
.fn()
@@ -63,15 +57,18 @@ describe('refinementList()', () => {
// Given
const cssClasses = {
root: ['root', 'cx'],
- header: 'header',
- body: 'body',
- footer: 'footer',
+ noRefinementRoot: 'noRefinementRoot',
list: 'list',
item: 'item',
- active: 'active',
+ selectedItem: 'selectedItem',
+ searchBox: 'searchBox',
label: 'label',
checkbox: 'checkbox',
+ labelText: 'labelText',
count: 'count',
+ noResults: 'noResults',
+ showMore: 'showMore',
+ disabledShowMore: 'disabledShowMore',
};
// When
@@ -79,108 +76,45 @@ describe('refinementList()', () => {
const actual = ReactDOM.render.mock.calls[0][0].props.cssClasses;
// Then
- expect(actual.root).toBe('ais-refinement-list root cx');
- expect(actual.header).toBe('ais-refinement-list--header header');
- expect(actual.body).toBe('ais-refinement-list--body body');
- expect(actual.footer).toBe('ais-refinement-list--footer footer');
- expect(actual.list).toBe('ais-refinement-list--list list');
- expect(actual.item).toBe('ais-refinement-list--item item');
- expect(actual.active).toBe('ais-refinement-list--item__active active');
- expect(actual.label).toBe('ais-refinement-list--label label');
- expect(actual.checkbox).toBe('ais-refinement-list--checkbox checkbox');
- expect(actual.count).toBe('ais-refinement-list--count count');
- });
- });
-
- describe('autoHideContainer', () => {
- it('should set shouldAutoHideContainer to false if there are facetValues', () => {
- // Given
- results.getFacetValues = jest
- .fn()
- .mockReturnValue([{ name: 'foo' }, { name: 'bar' }]);
-
- // When
- renderWidget();
- const actual =
- ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer;
-
- // Then
- expect(actual).toBe(false);
- });
- it('should set shouldAutoHideContainer to true if no facet values', () => {
- // Given
- results.getFacetValues = jest.fn().mockReturnValue([]);
-
- // When
- renderWidget();
- const actual =
- ReactDOM.render.mock.calls[0][0].props.shouldAutoHideContainer;
-
- // Then
- expect(actual).toBe(true);
- });
- });
-
- describe('header', () => {
- it('should pass the refined count to the header data', () => {
- // Given
- const facetValues = [
- {
- name: 'foo',
- isRefined: true,
- },
- {
- name: 'bar',
- isRefined: true,
- },
- {
- name: 'baz',
- isRefined: false,
- },
- ];
- results.getFacetValues = jest.fn().mockReturnValue(facetValues);
-
- // When
- renderWidget();
- const props = ReactDOM.render.mock.calls[0][0].props;
-
- // Then
- expect(props.headerFooterData.header.refinedFacetsCount).toEqual(2);
- });
-
- it('should dynamically update the header template on subsequent renders', () => {
- // Given
- const widgetOptions = { container, attributeName: 'type' };
- const initOptions = { helper, createURL, instantSearchInstance };
- const facetValues = [
- {
- name: 'foo',
- isRefined: true,
- },
- {
- name: 'bar',
- isRefined: false,
- },
- ];
- results.getFacetValues = jest.fn().mockReturnValue(facetValues);
- const renderOptions = { results, helper, state };
-
- // When
- widget = refinementList(widgetOptions);
- widget.init(initOptions);
- widget.render(renderOptions);
-
- // Then
- let props = ReactDOM.render.mock.calls[0][0].props;
- expect(props.headerFooterData.header.refinedFacetsCount).toEqual(1);
-
- // When... second render call
- facetValues[1].isRefined = true;
- widget.render(renderOptions);
-
- // Then
- props = ReactDOM.render.mock.calls[1][0].props;
- expect(props.headerFooterData.header.refinedFacetsCount).toEqual(2);
+ expect(actual.root).toMatchInlineSnapshot(
+ `"ais-RefinementList root cx"`
+ );
+ expect(actual.noRefinementRoot).toMatchInlineSnapshot(
+ `"ais-RefinementList--noRefinement noRefinementRoot"`
+ );
+ expect(actual.list).toMatchInlineSnapshot(
+ `"ais-RefinementList-list list"`
+ );
+ expect(actual.item).toMatchInlineSnapshot(
+ `"ais-RefinementList-item item"`
+ );
+ expect(actual.selectedItem).toMatchInlineSnapshot(
+ `"ais-RefinementList-item--selected selectedItem"`
+ );
+ expect(actual.searchBox).toMatchInlineSnapshot(
+ `"ais-RefinementList-searchBox searchBox"`
+ );
+ expect(actual.label).toMatchInlineSnapshot(
+ `"ais-RefinementList-label label"`
+ );
+ expect(actual.checkbox).toMatchInlineSnapshot(
+ `"ais-RefinementList-checkbox checkbox"`
+ );
+ expect(actual.labelText).toMatchInlineSnapshot(
+ `"ais-RefinementList-labelText labelText"`
+ );
+ expect(actual.count).toMatchInlineSnapshot(
+ `"ais-RefinementList-count count"`
+ );
+ expect(actual.noResults).toMatchInlineSnapshot(
+ `"ais-RefinementList-noResults noResults"`
+ );
+ expect(actual.showMore).toMatchInlineSnapshot(
+ `"ais-RefinementList-showMore showMore"`
+ );
+ expect(actual.disabledShowMore).toMatchInlineSnapshot(
+ `"ais-RefinementList-showMore--disabled disabledShowMore"`
+ );
});
});
@@ -203,36 +137,37 @@ describe('refinementList()', () => {
});
describe('show more', () => {
- it('should return a configuration with the highest limit value (default value)', () => {
+ it('should return a configuration with the same top-level limit value (default value)', () => {
const opts = {
container,
- attributeName: 'attributeName',
+ attribute: 'attribute',
limit: 1,
- showMore: {},
};
const wdgt = refinementList(opts);
const partialConfig = wdgt.getConfiguration({});
- expect(partialConfig.maxValuesPerFacet).toBe(100);
+ expect(partialConfig.maxValuesPerFacet).toBe(1);
});
it('should return a configuration with the highest limit value (custom value)', () => {
const opts = {
container,
- attributeName: 'attributeName',
+ attribute: 'attribute',
limit: 1,
- showMore: { limit: 99 },
+ showMore: true,
+ showMoreLimit: 99,
};
const wdgt = refinementList(opts);
const partialConfig = wdgt.getConfiguration({});
- expect(partialConfig.maxValuesPerFacet).toBe(opts.showMore.limit);
+ expect(partialConfig.maxValuesPerFacet).toBe(opts.showMoreLimit);
});
it('should not accept a show more limit that is < limit', () => {
const opts = {
container,
- attributeName: 'attributeName',
+ attribute: 'attribute',
limit: 100,
- showMore: { limit: 1 },
+ showMore: true,
+ showMoreLimit: 1,
};
expect(() => refinementList(opts)).toThrow();
});
diff --git a/src/widgets/refinement-list/defaultTemplates.js b/src/widgets/refinement-list/defaultTemplates.js
index 1238084e3b..e63761c744 100644
--- a/src/widgets/refinement-list/defaultTemplates.js
+++ b/src/widgets/refinement-list/defaultTemplates.js
@@ -1,12 +1,19 @@
export default {
- header: '',
item: `
- {{{highlighted}}}
+ {{{highlighted}}}
{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
`,
- footer: '',
+ showMoreText: `
+ {{#isShowingMore}}
+ Show less
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ Show more
+ {{/isShowingMore}}
+ `,
+ searchableNoResults: 'No results',
};
diff --git a/src/widgets/refinement-list/defaultTemplates.searchForFacetValue.js b/src/widgets/refinement-list/defaultTemplates.searchForFacetValue.js
deleted file mode 100644
index 8d89e87b81..0000000000
--- a/src/widgets/refinement-list/defaultTemplates.searchForFacetValue.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- noResults: 'No results',
-};
diff --git a/src/widgets/refinement-list/refinement-list.js b/src/widgets/refinement-list/refinement-list.js
index f6b5442f60..c4f8d2a96c 100644
--- a/src/widgets/refinement-list/refinement-list.js
+++ b/src/widgets/refinement-list/refinement-list.js
@@ -1,32 +1,22 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-import filter from 'lodash/filter';
-
import RefinementList from '../../components/RefinementList/RefinementList.js';
import connectRefinementList from '../../connectors/refinement-list/connectRefinementList.js';
import defaultTemplates from './defaultTemplates.js';
-import sffvDefaultTemplates from './defaultTemplates.searchForFacetValue.js';
-import getShowMoreConfig from '../../lib/show-more/getShowMoreConfig.js';
-
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
- prefixKeys,
-} from '../../lib/utils.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-const bem = bemHelper('ais-refinement-list');
+const suit = component('RefinementList');
const renderer = ({
containerNode,
cssClasses,
- transformData,
templates,
renderState,
- collapsible,
- autoHideContainer,
- showMoreConfig,
- searchForFacetValues,
+ showMore,
+ searchable,
+ searchablePlaceholder,
+ searchableIsAlwaysActive,
}) => (
{
refine,
@@ -35,7 +25,6 @@ const renderer = ({
searchForItems,
isFromSearch,
instantSearchInstance,
- canRefine,
toggleShowMore,
isShowingMore,
hasExhaustiveItems,
@@ -45,7 +34,6 @@ const renderer = ({
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
@@ -53,31 +41,21 @@ const renderer = ({
return;
}
- // Pass count of currently selected items to the header template
- const headerFooterData = {
- header: { refinedFacetsCount: filter(items, { isRefined: true }).length },
- };
-
render(
,
containerNode
@@ -87,53 +65,26 @@ const renderer = ({
const usage = `Usage:
refinementList({
container,
- attributeName,
+ attribute,
[ operator='or' ],
- [ sortBy=['isRefined', 'count:desc', 'name:asc'] ],
- [ limit=10 ],
- [ cssClasses.{root, header, body, footer, list, item, active, label, checkbox, count}],
- [ templates.{header,item,footer} ],
- [ transformData.{item} ],
- [ autoHideContainer=true ],
- [ collapsible=false ],
- [ showMore.{templates: {active, inactive}, limit} ],
- [ collapsible=false ],
- [ searchForFacetValues.{placeholder, templates: {noResults}, isAlwaysActive, escapeFacetValues}],
+ [ sortBy = ['isRefined', 'count:desc', 'name:asc'] ],
+ [ limit = 10 ],
+ [ showMore = false],
+ [ showMoreLimit = 20 ],
+ [ cssClasses.{root, noRefinementRoot, searchBox, list, item, selectedItem, label, checkbox, labelText, count, noResults, showMore, disabledShowMore}],
+ [ templates.{item, searchableNoResults, showMoreText} ],
+ [ searchable ],
+ [ searchablePlaceholder ],
+ [ searchableIsAlwaysActive = true ],
+ [ searchableEscapeFacetValues = true ],
[ transformItems ],
})`;
-/**
- * @typedef {Object} SearchForFacetTemplates
- * @property {string} [noResults] Templates to use for search for facet values.
- */
-
-/**
- * @typedef {Object} SearchForFacetOptions
- * @property {string} [placeholder] Value of the search field placeholder.
- * @property {SearchForFacetTemplates} [templates] Templates to use for search for facet values.
- * @property {boolean} [isAlwaysActive=false] When `false` the search field will become disabled if
- * there are less items to display than the `options.limit`, otherwise the search field is always usable.
- * @property {boolean} [escapeFacetValues=false] When activated, it will escape the facet values that are returned
- * from Algolia. In this case, the surrounding tags will always be ` `.
- */
-
-/**
- * @typedef {Object} RefinementListShowMoreTemplates
- * @property {string} [active] Template used when showMore was clicked.
- * @property {string} [inactive] Template used when showMore not clicked.
- */
-
-/**
- * @typedef {Object} RefinementListShowMoreOptions
- * @property {RefinementListShowMoreTemplates} [templates] Templates to use for showMore.
- * @property {number} [limit] Max number of facets values to display when showMore is clicked.
- */
-
/**
* @typedef {Object} RefinementListTemplates
- * @property {string|function(object):string} [header] Header template, provided with `refinedFacetsCount` data property.
* @property {string|function(RefinementListItemData):string} [item] Item template, provided with `label`, `highlighted`, `value`, `count`, `isRefined`, `url` data properties.
- * @property {string|function} [footer] Footer template.
+ * @property {string|function} [searchableNoResults] Templates to use for search for facet values.
+ * @property {string|function} [showMoreText] Template used for the show more text, provided with `isShowingMore` data property.
*/
/**
@@ -147,47 +98,42 @@ refinementList({
* @property {object} cssClasses Object containing all the classes computed for the item.
*/
-/**
- * @typedef {Object} RefinementListTransforms
- * @property {function} [item] Function to change the object passed to the `item` template.
- */
-
/**
* @typedef {Object} RefinementListCSSClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
+ * @property {string|string[]} [noRefinementRoot] CSS class to add to the root element when no refinements.
+ * @property {string|string[]} [noResults] CSS class to add to the root element with no results.
* @property {string|string[]} [list] CSS class to add to the list element.
* @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [active] CSS class to add to each active element.
+ * @property {string|string[]} [selectedItem] CSS class to add to each selected element.
* @property {string|string[]} [label] CSS class to add to each label element (when using the default template).
* @property {string|string[]} [checkbox] CSS class to add to each checkbox element (when using the default template).
+ * @property {string|string[]} [labelText] CSS class to add to each label text element.
+ * @property {string|string[]} [showMore] CSS class to add to the show more element
+ * @property {string|string[]} [disabledShowMore] CSS class to add to the disabledshow more element
* @property {string|string[]} [count] CSS class to add to each count element (when using the default template).
*/
-/**
- * @typedef {Object} RefinementListCollapsibleOptions
- * @property {boolean} [collapsed] Initial collapsed state of a collapsible widget.
- */
-
/**
* @typedef {Object} RefinementListWidgetOptions
* @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {string} attributeName Name of the attribute for faceting.
+ * @property {string} attribute Name of the attribute for faceting.
* @property {"and"|"or"} [operator="or"] How to apply refinements. Possible values: `or`, `and`
* @property {string[]|function} [sortBy=["isRefined", "count:desc", "name:asc"]] How to sort refinements. Possible values: `count:asc` `count:desc` `name:asc` `name:desc` `isRefined`.
*
* You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
* @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- * @property {number} [limit=10] How much facet values to get. When the show more feature is activated this is the minimum number of facets requested (the show more button is not in active state).
- * @property {SearchForFacetOptions|boolean} [searchForFacetValues=false] Add a search input to let the user search for more facet values. In order to make this feature work, you need to make the attribute searchable [using the API](https://www.algolia.com/doc/guides/searching/faceting/?language=js#declaring-a-searchable-attribute-for-faceting) or [the dashboard](https://www.algolia.com/explorer/display/).
- * @property {RefinementListShowMoreOptions|boolean} [showMore=false] Limit the number of results and display a showMore button.
+ * @property {boolean} [searchable=false] Add a search input to let the user search for more facet values. In order to make this feature work, you need to make the attribute searchable [using the API](https://www.algolia.com/doc/guides/searching/faceting/?language=js#declaring-a-searchable-attribute-for-faceting) or [the dashboard](https://www.algolia.com/explorer/display/).
+ * @property {number} [limit = 10] The minimum number of facet values to retrieve.
+ * @property {boolean} [showMore = false] Whether to display a button that expands the number of items.
+ * @property {number} [showMoreLimit = 20] The max number of items to display if the widget
+ * @property {string} [searchablePlaceholder] Value of the search field placeholder.
+ * @property {boolean} [searchableIsAlwaysActive=true] When `false` the search field will become disabled if
+ * there are less items to display than the `options.limit`, otherwise the search field is always usable.
+ * @property {boolean} [searchableEscapeFacetValues=true] When activated, it will escape the facet values that are returned
+ * from Algolia. In this case, the surrounding tags will always be ` `.
* @property {RefinementListTemplates} [templates] Templates to use for the widget.
- * @property {RefinementListTransforms} [transformData] Functions to update the values before applying the templates.
- * @property {boolean} [autoHideContainer=true] Hide the container when no items in the refinement list.
* @property {RefinementListCSSClasses} [cssClasses] CSS classes to add to the wrapping elements.
- * @property {RefinementListCollapsibleOptions|boolean} [collapsible=false] If true, the user can collapse the widget. If the use clicks on the header, it will hide the content and the footer.
*/
/**
@@ -203,7 +149,7 @@ refinementList({
*
* @requirements
*
- * The attribute passed to `attributeName` must be declared as an
+ * The attribute passed to `attribute` must be declared as an
* [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
* in your Algolia settings.
*
@@ -218,79 +164,84 @@ refinementList({
* search.addWidget(
* instantsearch.widgets.refinementList({
* container: '#brands',
- * attributeName: 'brand',
+ * attribute: 'brand',
* operator: 'or',
* limit: 10,
- * templates: {
- * header: 'Brands'
- * }
* })
* );
*/
export default function refinementList({
container,
- attributeName,
- operator = 'or',
- sortBy = ['isRefined', 'count:desc', 'name:asc'],
- limit = 10,
+ attribute,
+ operator,
+ sortBy,
+ limit,
+ showMore,
+ showMoreLimit,
+ searchable = false,
+ searchablePlaceholder = 'Search...',
+ searchableEscapeFacetValues = true,
+ searchableIsAlwaysActive = true,
cssClasses: userCssClasses = {},
templates = defaultTemplates,
- collapsible = false,
- transformData,
- autoHideContainer = true,
- showMore = false,
- searchForFacetValues = false,
transformItems,
} = {}) {
if (!container) {
throw new Error(usage);
}
- const showMoreConfig = getShowMoreConfig(showMore);
- if (showMoreConfig && showMoreConfig.limit < limit) {
- throw new Error('showMore.limit configuration should be > than the limit in the main configuration'); // eslint-disable-line
- }
-
- const escapeFacetValues = searchForFacetValues
- ? Boolean(searchForFacetValues.escapeFacetValues)
+ const escapeFacetValues = searchable
+ ? Boolean(searchableEscapeFacetValues)
: false;
- const showMoreLimit = (showMoreConfig && showMoreConfig.limit) || limit;
const containerNode = getContainerNode(container);
- const showMoreTemplates = showMoreConfig
- ? prefixKeys('show-more-', showMoreConfig.templates)
- : {};
- const searchForValuesTemplates = searchForFacetValues
- ? searchForFacetValues.templates || sffvDefaultTemplates
- : {};
const allTemplates = {
+ ...defaultTemplates,
...templates,
- ...showMoreTemplates,
- ...searchForValuesTemplates,
};
const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- active: cx(bem('item', 'active'), userCssClasses.active),
- label: cx(bem('label'), userCssClasses.label),
- checkbox: cx(bem('checkbox'), userCssClasses.checkbox),
- count: cx(bem('count'), userCssClasses.count),
+ root: cx(suit(), userCssClasses.root),
+ noRefinementRoot: cx(
+ suit({ modifierName: 'noRefinement' }),
+ userCssClasses.noRefinementRoot
+ ),
+ list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
+ item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
+ selectedItem: cx(
+ suit({ descendantName: 'item', modifierName: 'selected' }),
+ userCssClasses.selectedItem
+ ),
+ searchBox: cx(
+ suit({ descendantName: 'searchBox' }),
+ userCssClasses.searchBox
+ ),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ checkbox: cx(suit({ descendantName: 'checkbox' }), userCssClasses.checkbox),
+ labelText: cx(
+ suit({ descendantName: 'labelText' }),
+ userCssClasses.labelText
+ ),
+ count: cx(suit({ descendantName: 'count' }), userCssClasses.count),
+ noResults: cx(
+ suit({ descendantName: 'noResults' }),
+ userCssClasses.noResults
+ ),
+ showMore: cx(suit({ descendantName: 'showMore' }), userCssClasses.showMore),
+ disabledShowMore: cx(
+ suit({ descendantName: 'showMore', modifierName: 'disabled' }),
+ userCssClasses.disabledShowMore
+ ),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
- transformData,
templates: allTemplates,
renderState: {},
- collapsible,
- autoHideContainer,
- showMoreConfig,
- searchForFacetValues,
+ searchable,
+ searchablePlaceholder,
+ searchableIsAlwaysActive,
+ showMore,
});
try {
@@ -298,15 +249,16 @@ export default function refinementList({
unmountComponentAtNode(containerNode)
);
return makeWidget({
- attributeName,
+ attribute,
operator,
limit,
+ showMore,
showMoreLimit,
sortBy,
escapeFacetValues,
transformItems,
});
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/search-box/__tests__/__snapshots__/search-box-test.js.snap b/src/widgets/search-box/__tests__/__snapshots__/search-box-test.js.snap
new file mode 100644
index 0000000000..b0b7a61252
--- /dev/null
+++ b/src/widgets/search-box/__tests__/__snapshots__/search-box-test.js.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`searchBox() markup renders correctly 1`] = `
+
+`;
diff --git a/src/widgets/search-box/__tests__/search-box-test.js b/src/widgets/search-box/__tests__/search-box-test.js
index 15a89dc55f..9ade2ad763 100644
--- a/src/widgets/search-box/__tests__/search-box-test.js
+++ b/src/widgets/search-box/__tests__/search-box-test.js
@@ -1,12 +1,6 @@
import searchBox from '../search-box';
import EventEmitter from 'events';
-function createHTMLNodeFromString(string) {
- const parent = document.createElement('div');
- parent.innerHTML = string;
- return parent.firstChild;
-}
-
const onHistoryChange = () => {};
describe('searchBox()', () => {
@@ -55,17 +49,15 @@ describe('searchBox()', () => {
it('add a reset button inside the div', () => {
widget = searchBox(opts);
widget.init({ state, helper, onHistoryChange });
- const button = container.getElementsByTagName('button');
+ const button = container.querySelectorAll('button[type="reset"]');
expect(button).toHaveLength(1);
});
- it('add a magnifier inside the div', () => {
+ it('add a submit inside the div', () => {
widget = searchBox(opts);
widget.init({ state, helper, onHistoryChange });
- const magnifier = container.getElementsByClassName(
- 'ais-search-box--magnifier'
- );
- expect(magnifier).toHaveLength(1);
+ const submit = container.querySelectorAll('button[type="submit"]');
+ expect(submit).toHaveLength(1);
});
it('sets default HTML attribute to the input', () => {
@@ -75,7 +67,7 @@ describe('searchBox()', () => {
expect(input.getAttribute('autocapitalize')).toEqual('off');
expect(input.getAttribute('autocomplete')).toEqual('off');
expect(input.getAttribute('autocorrect')).toEqual('off');
- expect(input.getAttribute('class')).toEqual('ais-search-box--input');
+ expect(input.getAttribute('class')).toEqual('ais-SearchBox-input');
expect(input.getAttribute('placeholder')).toEqual('');
expect(input.getAttribute('role')).toEqual('textbox');
expect(input.getAttribute('spellcheck')).toEqual('false');
@@ -84,70 +76,51 @@ describe('searchBox()', () => {
it('supports cssClasses option', () => {
opts.cssClasses = {
- root: ['root-class', 'cx'],
- input: 'input-class',
+ root: ['root', 'customRoot'],
+ form: 'form',
+ input: 'input',
+ submit: 'submit',
+ reset: 'reset',
+ resetIcon: 'resetIcon',
+ loadingIndicator: 'loadingIndicator',
+ loadingIcon: 'loadingIcon',
};
widget = searchBox(opts);
widget.init({ state, helper, onHistoryChange });
- const actualRootClasses = container
- .querySelector('input')
- .parentNode.getAttribute('class');
- const actualInputClasses = container
- .querySelector('input')
- .getAttribute('class');
- const expectedRootClasses = 'ais-search-box root-class cx';
- const expectedInputClasses = 'ais-search-box--input input-class';
-
- expect(actualRootClasses).toEqual(expectedRootClasses);
- expect(actualInputClasses).toEqual(expectedInputClasses);
- });
- });
-
- describe('targeting an input', () => {
- it('reuse the existing input', () => {
- container = document.body.appendChild(document.createElement('input'));
- widget = searchBox({ container });
- widget.init({ state, helper, onHistoryChange });
- expect(container.tagName).toEqual('INPUT');
- expect(container.getAttribute('autocapitalize')).toEqual('off');
- expect(container.getAttribute('autocomplete')).toEqual('off');
- expect(container.getAttribute('autocorrect')).toEqual('off');
- expect(container.getAttribute('class')).toEqual('ais-search-box--input');
- expect(container.getAttribute('placeholder')).toEqual('');
- expect(container.getAttribute('role')).toEqual('textbox');
- expect(container.getAttribute('spellcheck')).toEqual('false');
- expect(container.getAttribute('type')).toEqual('text');
- });
+ widget.render({
+ state,
+ helper,
+ searchMetadata: { isSearchStalled: false },
+ });
- it('passes HTML attributes', () => {
- container = createHTMLNodeFromString(
- ' '
+ expect(container.querySelector('.ais-SearchBox').classList).toContain(
+ 'root'
);
- widget = searchBox({ container });
- widget.init({ state, helper, onHistoryChange });
- expect(container.getAttribute('id')).toEqual('foo');
- expect(container.getAttribute('class')).toEqual(
- 'my-class ais-search-box--input'
+ expect(container.querySelector('.ais-SearchBox').classList).toContain(
+ 'customRoot'
);
- expect(container.getAttribute('placeholder')).toEqual('Search');
- });
-
- it('supports cssClasses', () => {
- container = createHTMLNodeFromString(' ');
- widget = searchBox({
- container,
- cssClasses: { root: 'root-class', input: 'input-class' },
- });
- widget.init({ state, helper, onHistoryChange });
-
- const actualRootClasses = container.parentNode.getAttribute('class');
- const actualInputClasses = container.getAttribute('class');
- const expectedRootClasses = 'ais-search-box root-class';
- const expectedInputClasses = 'my-class ais-search-box--input input-class';
-
- expect(actualRootClasses).toEqual(expectedRootClasses);
- expect(actualInputClasses).toEqual(expectedInputClasses);
+ expect(
+ container.querySelector('.ais-SearchBox-form').classList
+ ).toContain('form');
+ expect(
+ container.querySelector('.ais-SearchBox-input').classList
+ ).toContain('input');
+ expect(
+ container.querySelector('.ais-SearchBox-submit').classList
+ ).toContain('submit');
+ expect(
+ container.querySelector('.ais-SearchBox-reset').classList
+ ).toContain('reset');
+ expect(
+ container.querySelector('.ais-SearchBox-resetIcon').classList
+ ).toContain('resetIcon');
+ expect(
+ container.querySelector('.ais-SearchBox-loadingIndicator').classList
+ ).toContain('loadingIndicator');
+ expect(
+ container.querySelector('.ais-SearchBox-loadingIcon').classList
+ ).toContain('loadingIcon');
});
});
@@ -161,50 +134,19 @@ describe('searchBox()', () => {
widget.init({ state, helper, onHistoryChange });
// Then
- const wrapper = container.querySelectorAll('div.ais-search-box')[0];
- const input = container.querySelectorAll('input')[0];
+ const wrapper = container.querySelector('.ais-SearchBox');
+ const input = container.querySelector('.ais-SearchBox-input');
expect(wrapper.contains(input)).toEqual(true);
- expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
- });
-
- it('when targeting an input', () => {
- // Given
- container = document.body.appendChild(document.createElement('input'));
- widget = searchBox({ container });
-
- // When
- widget.init({ state, helper, onHistoryChange });
-
- // Then
- const wrapper = container.parentNode;
- expect(wrapper.getAttribute('class')).toEqual('ais-search-box');
- });
-
- it('can be disabled with wrapInput:false', () => {
- // Given
- container = document.createElement('div');
- widget = searchBox({ container, wrapInput: false });
-
- // When
- widget.init({ state, helper, onHistoryChange });
-
- // Then
- const wrapper = container.querySelectorAll('div.ais-search-box');
- const input = container.querySelectorAll('input')[0];
- expect(wrapper).toHaveLength(0);
- expect(container.firstChild).toEqual(input);
});
});
describe('reset', () => {
let defaultInitOptions;
let defaultWidgetOptions;
- let $;
beforeEach(() => {
container = document.createElement('div');
- $ = container.querySelectorAll.bind(container);
defaultWidgetOptions = { container };
defaultInitOptions = { state, helper, onHistoryChange };
});
@@ -217,7 +159,8 @@ describe('searchBox()', () => {
widget.init(defaultInitOptions);
// Then
- expect($('.ais-search-box--reset-wrapper')[0].style.display).toBe('none');
+ const element = container.querySelector('.ais-SearchBox-reset');
+ expect(element.hasAttribute('hidden')).toBe(true);
});
it('should be shown when there is a query', () => {
@@ -229,9 +172,8 @@ describe('searchBox()', () => {
simulateInputEvent('test', 'tes', widget, helper, state, container);
// Then
- expect($('.ais-search-box--reset-wrapper')[0].style.display).toBe(
- 'block'
- );
+ const element = container.querySelector('.ais-SearchBox-reset');
+ expect(element.getAttribute('hidden')).toBe('');
});
it('should clear the query', () => {
@@ -240,20 +182,22 @@ describe('searchBox()', () => {
widget.init(defaultInitOptions);
simulateInputEvent('test', 'tes', widget, helper, state, container);
+ const element = container.querySelector('.ais-SearchBox-reset');
// When
- $('.ais-search-box--reset-wrapper')[0].click();
+ element.click();
// Then
expect(helper.setQuery).toHaveBeenCalled();
expect(helper.search).toHaveBeenCalled();
+ expect(document.activeElement).toBe(container.querySelector('input'));
});
it('should let the user define its own string template', () => {
// Given
widget = searchBox({
...defaultWidgetOptions,
- reset: {
- template: 'Foobar ',
+ templates: {
+ reset: 'Foobar ',
},
});
@@ -268,18 +212,18 @@ describe('searchBox()', () => {
// Given
widget = searchBox({
...defaultWidgetOptions,
- reset: false,
+ showReset: false,
});
// When
widget.init(defaultInitOptions);
// Then
- expect($('.ais-search-box--reset-wrapper')).toHaveLength(0);
+ expect(document.querySelectorAll('ais-SearchBox-reset')).toHaveLength(0);
});
});
- describe('magnifier', () => {
+ describe('submit', () => {
let defaultInitOptions;
let defaultWidgetOptions;
@@ -293,144 +237,7 @@ describe('searchBox()', () => {
// Given
widget = searchBox({
...defaultWidgetOptions,
- magnifier: {
- template: 'Foobar',
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect(container.innerHTML).toContain('Foobar');
- });
- });
-
- describe('poweredBy', () => {
- let defaultInitOptions;
- let defaultWidgetOptions;
- let $;
-
- beforeEach(() => {
- container = document.createElement('div');
- $ = container.querySelectorAll.bind(container);
- defaultWidgetOptions = { container };
- defaultInitOptions = { state, helper, onHistoryChange };
- });
-
- it('should not add the element with default options', () => {
- // Given
- widget = searchBox(defaultWidgetOptions);
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect($('.ais-search-box--powered-by')).toHaveLength(0);
- });
-
- it('should not add the element with poweredBy: false', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: false,
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect($('.ais-search-box--powered-by')).toHaveLength(0);
- });
-
- it('should add the element with poweredBy: true', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: true,
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect($('.ais-search-box--powered-by')).toHaveLength(1);
- });
-
- it('should contain a link to Algolia with poweredBy: true', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: true,
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- const actual = $('.ais-search-box--powered-by-link');
- const url = `https://www.algolia.com/?utm_source=instantsearch.js&utm_medium=website&utm_content=${
- location.hostname
- }&utm_campaign=poweredby`;
- expect(actual).toHaveLength(1);
- expect(actual[0].tagName).toEqual('A');
- expect(actual[0].innerHTML).toEqual('Algolia');
- expect(actual[0].getAttribute('href')).toEqual(url);
- });
-
- it('should let user add its own CSS classes with poweredBy.cssClasses', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- cssClasses: {
- root: 'myroot',
- link: 'mylink',
- },
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- const root = $('.myroot');
- const link = $('.mylink');
- expect(root).toHaveLength(1);
- expect(link).toHaveLength(1);
- expect(link[0].tagName).toEqual('A');
- expect(link[0].innerHTML).toEqual('Algolia');
- });
-
- it('should still apply default CSS classes even if user provides its own', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- cssClasses: {
- root: 'myroot',
- link: 'mylink',
- },
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- const root = $('.ais-search-box--powered-by');
- const link = $('.ais-search-box--powered-by-link');
- expect(root).toHaveLength(1);
- expect(link).toHaveLength(1);
- });
-
- it('should let the user define its own string template', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- template: '
Foobar
',
- },
+ templates: { submit: '
Foobar' },
});
// When
@@ -440,164 +247,26 @@ describe('searchBox()', () => {
expect(container.innerHTML).toContain('Foobar');
});
- it('should let the user define its own Hogan template', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- template: '
Foobar--{{url}}
',
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect(container.innerHTML).toContain('Foobar--https://www.algolia.com/');
- });
-
- it('should let the user define its own function template', () => {
+ it('should not be present if showSubmit is `false`', () => {
// Given
widget = searchBox({
...defaultWidgetOptions,
- poweredBy: {
- template: data => `
Foobar--${data.url}
`,
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect(container.innerHTML).toContain('Foobar--https://www.algolia.com/');
- });
-
- it('should gracefully handle templates with leading spaces', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- template: `
-
-
Foobar
`,
- },
+ showSubmit: false,
});
// When
widget.init(defaultInitOptions);
// Then
- expect(container.innerHTML).toContain('Foobar');
- });
-
- it('should handle templates not wrapped in a node', () => {
- // Given
- widget = searchBox({
- ...defaultWidgetOptions,
- poweredBy: {
- template: 'Foobar
',
- },
- });
-
- // When
- widget.init(defaultInitOptions);
-
- // Then
- expect(container.innerHTML).toContain('Foobar');
- expect($('.should-be-found')).toHaveLength(1);
- });
- });
-
- describe('input event listener', () => {
- beforeEach(() => {
- container = document.body.appendChild(document.createElement('input'));
- });
-
- describe('instant search', () => {
- beforeEach(() => {
- widget = searchBox({ container });
- });
-
- it('performs a search on any change', () => {
- simulateInputEvent('test', 'tes', widget, helper, state, container);
- expect(helper.search).toHaveBeenCalled();
- });
-
- it('sets the query on any change', () => {
- simulateInputEvent('test', 'tes', widget, helper, state, container);
- expect(helper.setQuery).toHaveBeenCalledTimes(1);
- });
-
- it('does nothing when query is the same as state', () => {
- simulateInputEvent('test', 'test', widget, helper, state, container);
- expect(helper.setQuery).not.toHaveBeenCalled();
- expect(helper.search).not.toHaveBeenCalled();
- });
- });
-
- describe('non-instant search and input event', () => {
- beforeEach(() => {
- widget = searchBox({ container, searchOnEnterKeyPressOnly: true });
- simulateInputEvent('test', 'tes', widget, helper, state, container);
- });
-
- it('updates the query', () => {
- expect(helper.setQuery).toHaveBeenCalledTimes(1);
- });
-
- it('does not search', () => {
- expect(helper.search).toHaveBeenCalledTimes(0);
- });
- });
-
- describe('using a queryHook', () => {
- it('calls the queryHook', () => {
- const queryHook = jest.fn();
- widget = searchBox({ container, queryHook });
- simulateInputEvent(
- 'queryhook input',
- 'tes',
- widget,
- helper,
- state,
- container
- );
- expect(queryHook).toHaveBeenCalledTimes(1);
- expect(queryHook).toHaveBeenLastCalledWith(
- 'queryhook input',
- expect.any(Function)
- );
- });
-
- it('does not perform a search by default', () => {
- const queryHook = jest.fn();
- widget = searchBox({ container, queryHook });
- simulateInputEvent('test', 'tes', widget, helper, state, container);
- expect(helper.setQuery).toHaveBeenCalledTimes(0);
- expect(helper.search).not.toHaveBeenCalled();
- });
-
- it('when calling the provided search function', () => {
- const queryHook = jest.fn((query, search) => search(query));
- widget = searchBox({ container, queryHook });
- simulateInputEvent('oh rly?', 'tes', widget, helper, state, container);
- expect(helper.setQuery).toHaveBeenCalledTimes(1);
- expect(helper.setQuery).toHaveBeenLastCalledWith('oh rly?');
- expect(helper.search).toHaveBeenCalled();
- });
-
- it('can override the query', () => {
- const queryHook = jest.fn((originalQuery, search) => search('hi mom!'));
- widget = searchBox({ container, queryHook });
- simulateInputEvent('come.on.', 'tes', widget, helper, state, container);
- expect(helper.setQuery).toHaveBeenLastCalledWith('hi mom!');
- });
+ expect(
+ container.querySelectorAll('.ais-search-box--submit')
+ ).toHaveLength(0);
});
});
describe('keyup', () => {
beforeEach(() => {
- container = document.body.appendChild(document.createElement('input'));
+ container = document.body.appendChild(document.createElement('div'));
});
describe('instant search', () => {
@@ -613,7 +282,7 @@ describe('searchBox()', () => {
describe('non-instant search', () => {
beforeEach(() => {
- widget = searchBox({ container, searchOnEnterKeyPressOnly: true });
+ widget = searchBox({ container, searchAsYouType: false });
helper.state.query = 'tes';
widget.init({ state: helper.state, helper, onHistoryChange });
});
@@ -621,30 +290,31 @@ describe('searchBox()', () => {
it('performs the search on keyup if
', () => {
// simulateInputEvent('test', 'tes', widget, helper, state, container);
// simulateKeyUpEvent({keyCode: 13}, widget, helper, state, container);
- container.value = 'test';
+ const input = container.querySelector('input');
+ input.value = 'test';
const e1 = new window.Event('input');
- container.dispatchEvent(e1);
+ input.dispatchEvent(e1);
expect(helper.setQuery).toHaveBeenCalledTimes(1);
expect(helper.search).toHaveBeenCalledTimes(0);
// setQuery is mocked and does not apply the modification of the helper
// we have to set it ourselves
- helper.state.query = container.value;
+ helper.state.query = input.value;
- const e2 = new window.Event('keyup', { keyCode: 13 });
- Object.defineProperty(e2, 'keyCode', { get: () => 13 });
- container.dispatchEvent(e2);
+ const e2 = new window.Event('submit');
+ input.parentElement.dispatchEvent(e2);
expect(helper.setQuery).toHaveBeenCalledTimes(1);
expect(helper.search).toHaveBeenCalledTimes(1);
});
it("doesn't perform the search on keyup if not ", () => {
- container.value = 'test';
+ const input = container.querySelector('input');
+ input.value = 'test';
const event = new window.Event('keyup', { keyCode: 42 });
Object.defineProperty(event, 'keyCode', { get: () => 42 });
- container.dispatchEvent(event);
+ input.dispatchEvent(event);
expect(helper.setQuery).toHaveBeenCalledTimes(0);
expect(helper.search).toHaveBeenCalledTimes(0);
@@ -654,7 +324,7 @@ describe('searchBox()', () => {
it('updates the input on history update', () => {
let cb;
- container = document.body.appendChild(document.createElement('input'));
+ container = document.body.appendChild(document.createElement('div'));
widget = searchBox({ container });
widget.init({
state,
@@ -663,68 +333,29 @@ describe('searchBox()', () => {
cb = fn;
},
});
- expect(container.value).toBe('');
- container.blur();
+ const input = container.querySelector('input');
+ expect(input.value).toBe('');
+ input.blur();
cb({ query: 'iphone' });
- expect(container.value).toBe('iphone');
+ expect(input.value).toBe('iphone');
});
it('handles external updates', () => {
- container = document.body.appendChild(document.createElement('input'));
- container.value = 'initial';
- widget = searchBox({ container });
- widget.init({ state, helper, onHistoryChange });
- container.blur();
- widget.render({
- helper: { state: { query: 'new value' } },
- searchMetadata: { isSearchStalled: false },
- });
- expect(container.value).toBe('new value');
- });
-
- it('does not update the input value when focused', () => {
- const input = document.createElement('input');
- container = document.body.appendChild(input);
- container.value = 'initial';
+ container = document.body.appendChild(document.createElement('div'));
widget = searchBox({ container });
widget.init({ state, helper, onHistoryChange });
- input.focus();
+ const input = container.querySelector('input');
+ input.blur();
widget.render({
helper: { state: { query: 'new value' } },
searchMetadata: { isSearchStalled: false },
});
- expect(container.value).toBe('initial');
+ expect(input.value).toBe('new value');
});
describe('autofocus', () => {
beforeEach(() => {
- container = document.body.appendChild(document.createElement('input'));
- container.focus = jest.fn();
- container.setSelectionRange = jest.fn();
- });
-
- describe('when auto', () => {
- beforeEach(() => {
- widget = searchBox({ container, autofocus: 'auto' });
- });
-
- it('is called if search is empty', () => {
- // Given
- helper.state.query = '';
- // When
- widget.init({ state, helper, onHistoryChange });
- // Then
- expect(container.focus).toHaveBeenCalled();
- });
-
- it('is not called if search is not empty', () => {
- // Given
- helper.state.query = 'foo';
- // When
- widget.init({ state, helper, onHistoryChange });
- // Then
- expect(container.focus).not.toHaveBeenCalled();
- });
+ container = document.body.appendChild(document.createElement('div'));
});
describe('when true', () => {
@@ -738,7 +369,7 @@ describe('searchBox()', () => {
// When
widget.init({ state, helper, onHistoryChange });
// Then
- expect(container.focus).toHaveBeenCalled();
+ expect(document.activeElement).toBe(container.querySelector('input'));
});
it('is called if search is not empty', () => {
@@ -747,7 +378,7 @@ describe('searchBox()', () => {
// When
widget.init({ state, helper, onHistoryChange });
// Then
- expect(container.focus).toHaveBeenCalled();
+ expect(document.activeElement).toBe(container.querySelector('input'));
});
it('forces cursor to be at the end of the query', () => {
@@ -756,7 +387,8 @@ describe('searchBox()', () => {
// When
widget.init({ state, helper, onHistoryChange });
// Then
- expect(container.setSelectionRange).toHaveBeenLastCalledWith(3, 3);
+ expect(container.querySelector('input').selectionStart).toEqual(3);
+ expect(container.querySelector('input').selectionEnd).toEqual(3);
});
});
@@ -771,7 +403,9 @@ describe('searchBox()', () => {
// When
widget.init({ state, helper, onHistoryChange });
// Then
- expect(container.focus).not.toHaveBeenCalled();
+ expect(document.activeElement).not.toBe(
+ container.querySelector('input')
+ );
});
it('is not called if search is not empty', () => {
@@ -780,10 +414,25 @@ describe('searchBox()', () => {
// When
widget.init({ state, helper, onHistoryChange });
// Then
- expect(container.focus).not.toHaveBeenCalled();
+ expect(document.activeElement).not.toBe(
+ container.querySelector('input')
+ );
});
});
});
+
+ describe('markup', () => {
+ it('renders correctly', () => {
+ container = document.createElement('div');
+ widget = searchBox({ container });
+
+ widget.init({ state, helper, onHistoryChange });
+
+ const wrapper = container.querySelector('.ais-SearchBox');
+
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
});
function simulateKeyUpEvent(args, widget, helper, state, container) {
@@ -803,7 +452,7 @@ function simulateInputEvent(
stateQuery,
widget,
helper,
- state,
+ _state,
container
) {
if (query === undefined) {
diff --git a/src/widgets/search-box/defaultTemplates.js b/src/widgets/search-box/defaultTemplates.js
index a9da5ba542..3000dfdb15 100644
--- a/src/widgets/search-box/defaultTemplates.js
+++ b/src/widgets/search-box/defaultTemplates.js
@@ -1,59 +1,33 @@
/* eslint max-len: 0 */
export default {
- poweredBy: `
-`,
reset: `
-
-
-
-
-
-
+
+
+
`,
- magnifier: `
-
+ submit: `
+
+
+
`,
loadingIndicator: `
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
`,
};
diff --git a/src/widgets/search-box/search-box.js b/src/widgets/search-box/search-box.js
index adbf371731..7988c0a93e 100644
--- a/src/widgets/search-box/search-box.js
+++ b/src/widgets/search-box/search-box.js
@@ -1,68 +1,43 @@
import forEach from 'lodash/forEach';
import cx from 'classnames';
-import { bemHelper, getContainerNode, renderTemplate } from '../../lib/utils';
+import { getContainerNode, renderTemplate } from '../../lib/utils';
import connectSearchBox from '../../connectors/search-box/connectSearchBox';
import defaultTemplates from './defaultTemplates';
+import { component } from '../../lib/suit';
-const bem = bemHelper('ais-search-box');
-const KEY_ENTER = 13;
-const KEY_SUPPRESS = 8;
+const suit = component('SearchBox');
const renderer = ({
containerNode,
cssClasses,
placeholder,
- poweredBy,
templates,
autofocus,
- searchOnEnterKeyPressOnly,
- wrapInput,
- reset,
- magnifier,
- loadingIndicator,
- // eslint-disable-next-line complexity
+ searchAsYouType,
+ showReset,
+ showSubmit,
+ showLoadingIndicator,
}) => (
{ refine, clear, query, onHistoryChange, isSearchStalled },
isFirstRendering
) => {
if (isFirstRendering) {
- const INPUT_EVENT = window.addEventListener ? 'input' : 'propertychange';
- const input = createInput(containerNode);
- const isInputTargeted = input === containerNode;
- let queryFromInput = query;
-
- if (isInputTargeted) {
- // To replace the node, we need to create an intermediate node
- const placeholderNode = document.createElement('div');
- input.parentNode.insertBefore(placeholderNode, input);
- const parentNode = input.parentNode;
- const wrappedInput = wrapInput ? wrapInputFn(input, cssClasses) : input;
- parentNode.replaceChild(wrappedInput, placeholderNode);
-
- const initialInputValue = input.value;
-
- // if the input contains a value, we provide it to the state
- if (initialInputValue) {
- queryFromInput = initialInputValue;
- refine(initialInputValue, false);
- }
- } else {
- const wrappedInput = wrapInput ? wrapInputFn(input, cssClasses) : input;
- containerNode.appendChild(wrappedInput);
- }
-
- if (magnifier) addMagnifier(input, magnifier, templates);
- if (reset) addReset(input, reset, templates, clear);
- if (loadingIndicator)
- addLoadingIndicator(input, loadingIndicator, templates);
+ const input = document.createElement('input');
+ const wrappedInput = wrapInputFn(input, cssClasses);
+ containerNode.appendChild(wrappedInput);
- addDefaultAttributesToInput(placeholder, input, queryFromInput, cssClasses);
-
- // Optional "powered by Algolia" widget
- if (poweredBy) {
- addPoweredBy(input, poweredBy, templates);
+ if (showSubmit) {
+ addSubmit(input, cssClasses, templates);
+ }
+ if (showReset) {
+ addReset(input, cssClasses, templates, clear);
+ }
+ if (showLoadingIndicator) {
+ addLoadingIndicator(input, cssClasses, templates);
}
+ addDefaultAttributesToInput(placeholder, input, query, cssClasses);
+
// When the page is coming from BFCache
// (https://developer.mozilla.org/en-US/docs/Working_with_BFCache)
// then we force the input value to be the current query
@@ -73,7 +48,7 @@ const renderer = ({
// - use back button
// - input query is empty (because autocomplete = off)
window.addEventListener('pageshow', () => {
- input.value = queryFromInput;
+ input.value = query;
});
// Update value when query change outside of the input
@@ -81,76 +56,70 @@ const renderer = ({
input.value = fullState.query || '';
});
- if (autofocus === true || (autofocus === 'auto' && queryFromInput === '')) {
+ if (autofocus === true) {
input.focus();
- input.setSelectionRange(queryFromInput.length, queryFromInput.length);
+ input.setSelectionRange(query.length, query.length);
}
- // search on enter
- if (searchOnEnterKeyPressOnly) {
- addListener(input, INPUT_EVENT, e => {
- refine(getValue(e), false);
+ const form = input.parentElement;
+
+ if (searchAsYouType) {
+ input.addEventListener('input', event => {
+ refine(event.currentTarget.value);
});
- addListener(input, 'keyup', e => {
- if (e.keyCode === KEY_ENTER) refine(getValue(e));
+ form.addEventListener('submit', event => {
+ event.preventDefault();
+ input.blur();
});
} else {
- addListener(input, INPUT_EVENT, getInputValueAndCall(refine));
-
- // handle IE8 weirdness where BACKSPACE key will not trigger an input change..
- // can be removed as soon as we remove support for it
- if (INPUT_EVENT === 'propertychange' || window.attachEvent) {
- addListener(
- input,
- 'keyup',
- ifKey(KEY_SUPPRESS, getInputValueAndCall(refine))
- );
- }
+ input.addEventListener('input', event => {
+ refine(event.currentTarget.value, false);
+ });
+ form.addEventListener('submit', event => {
+ refine(input.value);
+ event.preventDefault();
+ input.blur();
+ });
}
- } else {
- renderAfterInit({
- containerNode,
- query,
- loadingIndicator,
- isSearchStalled,
- });
- }
- if (reset) {
- const resetBtnSelector = `.${cx(bem('reset-wrapper'))}`;
- // hide reset button when there is no query
- const resetButton =
- containerNode.tagName === 'INPUT'
- ? containerNode.parentNode.querySelector(resetBtnSelector)
- : containerNode.querySelector(resetBtnSelector);
- resetButton.style.display = query && query.trim() ? 'block' : 'none';
+ return;
}
-};
-function renderAfterInit({
- containerNode,
- query,
- loadingIndicator,
- isSearchStalled,
-}) {
- const input = getInput(containerNode);
+ const input = containerNode.querySelector('input');
const isFocused = document.activeElement === input;
+
if (!isFocused && query !== input.value) {
input.value = query;
}
- if (loadingIndicator) {
- const rootElement =
- containerNode.tagName === 'INPUT'
- ? containerNode.parentNode
- : containerNode.firstChild;
- if (isSearchStalled) {
- rootElement.classList.add('ais-stalled-search');
- } else {
- rootElement.classList.remove('ais-stalled-search');
+ if (showLoadingIndicator) {
+ const loadingIndicatorElement = containerNode.querySelector(
+ `.${cssClasses.loadingIndicator}`
+ );
+
+ if (loadingIndicatorElement) {
+ if (isSearchStalled) {
+ loadingIndicatorElement.removeAttribute('hidden');
+ } else {
+ loadingIndicatorElement.setAttribute('hidden', '');
+ }
}
}
-}
+
+ if (showReset) {
+ const resetElement = containerNode.querySelector(`.${cssClasses.reset}`);
+
+ if (resetElement) {
+ const isUserTyping = Boolean(query && query.trim());
+
+ if (isUserTyping && !isSearchStalled) {
+ resetElement.removeAttribute('hidden');
+ } else {
+ resetElement.setAttribute('hidden', '');
+ }
+ }
+ }
+};
const disposer = containerNode => () => {
const range = document.createRange(); // IE10+
@@ -162,68 +131,52 @@ const usage = `Usage:
searchBox({
container,
[ placeholder ],
- [ cssClasses.{input,poweredBy} ],
- [ poweredBy=false || poweredBy.{template, cssClasses.{root,link}} ],
- [ wrapInput ],
- [ autofocus ],
- [ searchOnEnterKeyPressOnly ],
- [ queryHook ]
- [ reset=true || reset.{template, cssClasses.{root}} ]
+ [ cssClasses.{root, form, input, submit, submitIcon, reset, resetIcon, loadingIndicator, loadingIcon} ],
+ [ autofocus = false ],
+ [ searchAsYouType = true ],
+ [ showReset = true ],
+ [ showSubmit = true ],
+ [ showLoadingIndicator = true ],
+ [ queryHook ],
+ [ templates.{reset, submit, loadingIndicator} ],
})`;
/**
- * @typedef {Object} SearchBoxPoweredByCSSClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [link] CSS class to add to the link element.
- */
-
-/**
- * @typedef {Object} SearchBoxPoweredByOption
- * @property {function|string} template Template used for displaying the link. Can accept a function or a Hogan string.
- * @property {SearchBoxPoweredByCSSClasses} [cssClasses] CSS classes added to the powered-by badge.
- */
-
-/**
- * @typedef {Object} SearchBoxResetOption
- * @property {function|string} template Template used for displaying the button. Can accept a function or a Hogan string.
- * @property {{root: string}} [cssClasses] CSS classes added to the reset button.
- */
-
-/**
- * @typedef {Object} SearchBoxLoadingIndicatorOption
- * @property {function|string} template Template used for displaying the button. Can accept a function or a Hogan string.
- * @property {{root: string}} [cssClasses] CSS classes added to the loading-indicator element.
+ * @typedef {Ojbect} SearchBoxTemplates
+ * @property {function|string} submit Template used for displaying the submit. Can accept a function or a Hogan string.
+ * @property {function|string} reset Template used for displaying the button. Can accept a function or a Hogan string.
+ * @property {function|string} loadingIndicator Template used for displaying the button. Can accept a function or a Hogan string.
*/
/**
* @typedef {Object} SearchBoxCSSClasses
- * @property {string|string[]} [root] CSS class to add to the
- * wrapping `` (if `wrapInput` set to `true`).
- * @property {string|string[]} [input] CSS class to add to the input.
- */
-
-/**
- * @typedef {Object} SearchBoxMagnifierOption
- * @property {function|string} template Template used for displaying the magnifier. Can accept a function or a Hogan string.
- * @property {{root: string}} [cssClasses] CSS classes added to the magnifier.
+ * @property {string|string[]} [root] CSS class to add to the wrapping `
`
+ * @property {string|string[]} [form] CSS class to add to the form
+ * @property {string|string[]} [input] CSS class to add to the input.
+ * @property {string|string[]} [submit] CSS classes added to the submit button.
+ * @property {string|string[]} [submitIcon] CSS classes added to the submit icon.
+ * @property {string|string[]} [reset] CSS classes added to the reset button.
+ * @property {string|string[]} [resetIcon] CSS classes added to the reset icon.
+ * @property {string|string[]} [loadingIndicator] CSS classes added to the loading indicator element.
+ * @property {string|string[]} [loadingIcon] CSS classes added to the loading indicator icon.
*/
/**
* @typedef {Object} SearchBoxWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget. If the CSS selector or the HTMLElement is an existing input, the widget will use it.
- * @property {string} [placeholder] Input's placeholder.
- * @property {boolean|SearchBoxPoweredByOption} [poweredBy=false] Define if a "powered by Algolia" link should be added near the input.
- * @property {boolean|SearchBoxResetOption} [reset=true] Define if a reset button should be added in the input when there is a query.
- * @property {boolean|SearchBoxMagnifierOption} [magnifier=true] Define if a magnifier should be added at beginning of the input to indicate a search input.
- * @property {boolean|SearchBoxLoadingIndicatorOption} [loadingIndicator=false] Define if a loading indicator should be added at beginning of the input to indicate that search is currently stalled.
- * @property {boolean} [wrapInput=true] Wrap the input in a `div.ais-search-box`.
- * @property {boolean|string} [autofocus="auto"] autofocus on the input.
- * @property {boolean} [searchOnEnterKeyPressOnly=false] If set, trigger the search
+ * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget
+ * @property {string} [placeholder] The placeholder of the input
+ * @property {boolean} [autofocus=false] Whether the input should be autofocused
+ * @property {boolean} [searchAsYouType=true] If set, trigger the search
* once `
` is pressed only.
- * @property {SearchBoxCSSClasses} [cssClasses] CSS classes to add.
- * @property {function} [queryHook] A function that will be called every time a new search would be done. You
- * will get the query as first parameter and a search(query) function to call as the second parameter.
- * This queryHook can be used to debounce the number of searches done from the searchBox.
+ * @property {boolean} [showReset=true] Whether to show the reset button
+ * @property {boolean} [showSubmit=true] Whether to show the submit button
+ * @property {boolean} [showLoadingIndicator=true] Whether to show the loading indicator (replaces the submit if
+ * the search is stalled)
+ * @property {SearchBoxCSSClasses} [cssClasses] CSS classes to add
+ * @property {SearchBoxTemplates} [templates] Templates used for customizing the rendering of the searchbox
+ * @property {function} [queryHook] A function that is called every time a new search is done. You
+ * will get the query as the first parameter and a search (query) function to call as the second parameter.
+ * This `queryHook` can be used to debounce the number of searches done from the search box.
*/
/**
@@ -243,25 +196,20 @@ searchBox({
* instantsearch.widgets.searchBox({
* container: '#q',
* placeholder: 'Search for products',
- * autofocus: false,
- * poweredBy: true,
- * reset: true,
- * loadingIndicator: false
* })
* );
*/
export default function searchBox({
container,
placeholder = '',
- cssClasses = {},
- poweredBy = false,
- wrapInput = true,
- autofocus = 'auto',
- searchOnEnterKeyPressOnly = false,
- reset = true,
- magnifier = true,
- loadingIndicator = false,
+ cssClasses: userCssClasses = {},
+ autofocus = false,
+ searchAsYouType = true,
+ showReset = true,
+ showSubmit = true,
+ showLoadingIndicator = true,
queryHook,
+ templates,
} = {}) {
if (!container) {
throw new Error(usage);
@@ -269,28 +217,60 @@ export default function searchBox({
const containerNode = getContainerNode(container);
- // Only possible values are 'auto', true and false
- if (typeof autofocus !== 'boolean') {
- autofocus = 'auto';
+ if (containerNode.tagName === 'INPUT') {
+ // eslint-disable-next-line
+ // FIXME: the link should be updated when the documentation is migrated in the main Algolia doc
+ throw new Error(
+ `[InstantSearch.js] Since in version 3, \`container\` can not be an \`input\` anymore.
+
+Learn more in the [migration guide](https://community.algolia.com/instantsearch.js/v3/guides/v3-migration.html).`
+ );
}
- // Convert to object if only set to true
- if (poweredBy === true) {
- poweredBy = {};
+ // eslint-disable-next-line
+ // FIXME: the link should be updated when the documentation is migrated in the main Algolia doc
+ if (typeof autofocus !== 'boolean') {
+ throw new Error(
+ `[InstantSearch.js] Since in version 3, \`autofocus\` only supports boolean values.
+
+Learn more in the [migration guide](https://community.algolia.com/instantsearch.js/v3/guides/v3-migration.html).`
+ );
}
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ form: cx(suit({ descendantName: 'form' }), userCssClasses.form),
+ input: cx(suit({ descendantName: 'input' }), userCssClasses.input),
+ submit: cx(suit({ descendantName: 'submit' }), userCssClasses.submit),
+ submitIcon: cx(
+ suit({ descendantName: 'submitIcon' }),
+ userCssClasses.submitIcon
+ ),
+ reset: cx(suit({ descendantName: 'reset' }), userCssClasses.reset),
+ resetIcon: cx(
+ suit({ descendantName: 'resetIcon' }),
+ userCssClasses.resetIcon
+ ),
+ loadingIndicator: cx(
+ suit({ descendantName: 'loadingIndicator' }),
+ userCssClasses.loadingIndicator
+ ),
+ loadingIcon: cx(
+ suit({ descendantName: 'loadingIcon' }),
+ userCssClasses.loadingIcon
+ ),
+ };
+
const specializedRenderer = renderer({
containerNode,
cssClasses,
placeholder,
- poweredBy,
- templates: defaultTemplates,
+ templates: { ...defaultTemplates, ...templates },
autofocus,
- searchOnEnterKeyPressOnly,
- wrapInput,
- reset,
- magnifier,
- loadingIndicator,
+ searchAsYouType,
+ showReset,
+ showSubmit,
+ showLoadingIndicator,
});
try {
@@ -299,62 +279,11 @@ export default function searchBox({
disposer(containerNode)
);
return makeWidget({ queryHook });
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
-// the 'input' event is triggered when the input value changes
-// in any case: typing, copy pasting with mouse..
-// 'onpropertychange' is the IE8 alternative until we support IE8
-// but it's flawed: http://help.dottoro.com/ljhxklln.php
-
-function createInput(containerNode) {
- // Returns reference to targeted input if present, or create a new one
- if (containerNode.tagName === 'INPUT') {
- return containerNode;
- }
- return document.createElement('input');
-}
-
-function getInput(containerNode) {
- // Returns reference to targeted input if present, or look for it inside
- if (containerNode.tagName === 'INPUT') {
- return containerNode;
- }
- return containerNode.querySelector('input');
-}
-
-function wrapInputFn(input, cssClasses) {
- // Wrap input in a .ais-search-box div
- const wrapper = document.createElement('div');
- const CSSClassesToAdd = cx(bem(null), cssClasses.root).split(' ');
- CSSClassesToAdd.forEach(cssClass => wrapper.classList.add(cssClass));
- wrapper.appendChild(input);
- return wrapper;
-}
-
-function addListener(el, type, fn) {
- if (el.addEventListener) {
- el.addEventListener(type, fn);
- } else {
- el.attachEvent(`on${type}`, fn);
- }
-}
-
-function getValue(e) {
- return (e.currentTarget ? e.currentTarget : e.srcElement).value;
-}
-
-function ifKey(expectedKeyCode, func) {
- return actualEvent =>
- actualEvent.keyCode === expectedKeyCode && func(actualEvent);
-}
-
-function getInputValueAndCall(func) {
- return actualEvent => func(getValue(actualEvent));
-}
-
function addDefaultAttributesToInput(placeholder, input, query, cssClasses) {
const defaultAttributes = {
autocapitalize: 'off',
@@ -376,8 +305,7 @@ function addDefaultAttributesToInput(placeholder, input, query, cssClasses) {
});
// Add classes
- const CSSClassesToAdd = cx(bem('input'), cssClasses.input).split(' ');
- CSSClassesToAdd.forEach(cssClass => input.classList.add(cssClass));
+ input.className = cssClasses.input;
}
/**
@@ -385,152 +313,96 @@ function addDefaultAttributesToInput(placeholder, input, query, cssClasses) {
* it should reset the query.
* @private
* @param {HTMLElement} input the DOM node of the input of the searchbox
- * @param {object} reset the user options (cssClasses and template)
- * @param {object} $2 the default templates
+ * @param {object} cssClasses the object containing all the css classes
+ * @param {object} templates the templates object
* @param {function} clearFunction function called when the element is activated (clicked)
- * @returns {undefined} returns nothing
+ * @returns {undefined} Modifies the input
*/
-function addReset(input, reset, { reset: resetTemplate }, clearFunction) {
- reset = {
- cssClasses: {},
- template: resetTemplate,
- ...reset,
- };
-
- const resetCSSClasses = {
- root: cx(bem('reset'), reset.cssClasses.root),
- };
-
+function addReset(input, cssClasses, templates, clearFunction) {
const stringNode = renderTemplate({
- templateKey: 'template',
- templates: reset,
+ templateKey: 'reset',
+ templates,
data: {
- cssClasses: resetCSSClasses,
+ cssClasses,
},
});
- const htmlNode = createNodeFromString(stringNode, cx(bem('reset-wrapper')));
+ const node = document.createElement('button');
+ node.className = cssClasses.reset;
+ node.setAttribute('hidden', '');
+ node.type = 'reset';
+ node.title = 'Clear the search query';
+ node.innerHTML = stringNode;
- input.parentNode.appendChild(htmlNode);
+ input.parentNode.appendChild(node);
- htmlNode.addEventListener('click', event => {
- event.preventDefault();
+ node.addEventListener('click', () => {
+ input.focus();
clearFunction();
});
}
/**
- * Adds a magnifying glass in the searchbox widget
+ * Adds a button with a magnifying glass in the searchbox widget
* @private
* @param {HTMLElement} input the DOM node of the input of the searchbox
- * @param {object} magnifier the user options (cssClasses and template)
- * @param {object} $2 the default templates
- * @returns {undefined} returns nothing
+ * @param {object} cssClasses the user options (cssClasses and template)
+ * @param {object} templates the object containing all the templates
+ * @returns {undefined} Modifies the input
*/
-function addMagnifier(input, magnifier, { magnifier: magnifierTemplate }) {
- magnifier = {
- cssClasses: {},
- template: magnifierTemplate,
- ...magnifier,
- };
-
- const magnifierCSSClasses = {
- root: cx(bem('magnifier'), magnifier.cssClasses.root),
- };
-
+function addSubmit(input, cssClasses, templates) {
const stringNode = renderTemplate({
- templateKey: 'template',
- templates: magnifier,
+ templateKey: 'submit',
+ templates,
data: {
- cssClasses: magnifierCSSClasses,
+ cssClasses,
},
});
- const htmlNode = createNodeFromString(
- stringNode,
- cx(bem('magnifier-wrapper'))
- );
+ const node = document.createElement('button');
+ node.className = cssClasses.submit;
+ node.type = 'submit';
+ node.title = 'Submit the search query';
+ node.innerHTML = stringNode;
- input.parentNode.appendChild(htmlNode);
+ input.parentNode.appendChild(node);
}
-function addLoadingIndicator(
- input,
- loadingIndicator,
- { loadingIndicator: loadingIndicatorTemplate }
-) {
- loadingIndicator = {
- cssClasses: {},
- template: loadingIndicatorTemplate,
- ...loadingIndicator,
- };
-
- const loadingIndicatorCSSClasses = {
- root: cx(bem('loading-indicator'), loadingIndicator.cssClasses.root),
- };
-
+/**
+ * Adds a loading indicator (spinner) to the search box
+ * @param {DomElement} input DOM element where to add the loading indicator
+ * @param {Object} cssClasses css classes definition
+ * @param {Object} templates templates of the widget
+ * @returns {undefined} Modifies the input
+ */
+function addLoadingIndicator(input, cssClasses, templates) {
const stringNode = renderTemplate({
- templateKey: 'template',
- templates: loadingIndicator,
+ templateKey: 'loadingIndicator',
+ templates,
data: {
- cssClasses: loadingIndicatorCSSClasses,
+ cssClasses,
},
});
- const htmlNode = createNodeFromString(
- stringNode,
- cx(bem('loading-indicator-wrapper'))
- );
+ const node = document.createElement('span');
+ node.setAttribute('hidden', '');
+ node.className = cssClasses.loadingIndicator;
+ node.innerHTML = stringNode;
- input.parentNode.appendChild(htmlNode);
+ input.parentNode.appendChild(node);
}
-/**
- * Adds a powered by in the searchbox widget
- * @private
- * @param {HTMLElement} input the DOM node of the input of the searchbox
- * @param {object} poweredBy the user options (cssClasses and template)
- * @param {object} templates the default templates
- * @returns {undefined} returns nothing
- */
-function addPoweredBy(input, poweredBy, { poweredBy: poweredbyTemplate }) {
- // Default values
- poweredBy = {
- cssClasses: {},
- template: poweredbyTemplate,
- ...poweredBy,
- };
-
- const poweredByCSSClasses = {
- root: cx(bem('powered-by'), poweredBy.cssClasses.root),
- link: cx(bem('powered-by-link'), poweredBy.cssClasses.link),
- };
-
- const url =
- 'https://www.algolia.com/?' +
- 'utm_source=instantsearch.js&' +
- 'utm_medium=website&' +
- `utm_content=${location.hostname}&` +
- 'utm_campaign=poweredby';
-
- const stringNode = renderTemplate({
- templateKey: 'template',
- templates: poweredBy,
- data: {
- cssClasses: poweredByCSSClasses,
- url,
- },
- });
+function wrapInputFn(input, cssClasses) {
+ const wrapper = document.createElement('div');
+ wrapper.className = cssClasses.root;
- const htmlNode = createNodeFromString(stringNode);
+ const form = document.createElement('form');
+ form.className = cssClasses.form;
+ form.noValidate = true;
+ form.action = ''; // show search button on iOS keyboard
- input.parentNode.insertBefore(htmlNode, input.nextSibling);
-}
+ form.appendChild(input);
+ wrapper.appendChild(form);
-// Cross-browser way to create a DOM node from a string. We wrap in
-// a `span` to make sure we have one and only one node.
-function createNodeFromString(stringNode, rootClassname = '') {
- const tmpNode = document.createElement('div');
- tmpNode.innerHTML = `${stringNode.trim()} `;
- return tmpNode.firstChild;
+ return wrapper;
}
diff --git a/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap b/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap
deleted file mode 100644
index 3aeb8f1b57..0000000000
--- a/src/widgets/sort-by-selector/__tests__/__snapshots__/sort-by-selector-test.js.snap
+++ /dev/null
@@ -1,84 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`sortBySelector() calls twice ReactDOM.render( , container) 1`] = `
-
-`;
-
-exports[`sortBySelector() calls twice ReactDOM.render( , container) 2`] = `
-
-`;
-
-exports[`sortBySelector() renders transformed items 1`] = `
-
-`;
diff --git a/src/widgets/sort-by-selector/sort-by-selector.js b/src/widgets/sort-by-selector/sort-by-selector.js
deleted file mode 100644
index 40882f08cf..0000000000
--- a/src/widgets/sort-by-selector/sort-by-selector.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import Selector from '../../components/Selector.js';
-import connectSortBySelector from '../../connectors/sort-by-selector/connectSortBySelector.js';
-import { bemHelper, getContainerNode } from '../../lib/utils.js';
-
-const bem = bemHelper('ais-sort-by-selector');
-
-const renderer = ({ containerNode, cssClasses, autoHideContainer }) => (
- { currentRefinement, options, refine, hasNoResults },
- isFirstRendering
-) => {
- if (isFirstRendering) return;
-
- const shouldAutoHideContainer = autoHideContainer && hasNoResults;
-
- render(
- ,
- containerNode
- );
-};
-
-const usage = `Usage:
-sortBySelector({
- container,
- indices,
- [cssClasses.{root,select,item}={}],
- [autoHideContainer=false],
- [transformItems]
-})`;
-
-/**
- * @typedef {Object} SortByWidgetCssClasses
- * @property {string|string[]} [root] CSS classes added to the outer ``.
- * @property {string|string[]} [select] CSS classes added to the parent `
`.
- * @property {string|string[]} [item] CSS classes added to each ``.
- */
-
-/**
- * @typedef {Object} SortByIndexDefinition
- * @property {string} name The name of the index in Algolia.
- * @property {string} label The name of the index, for user usage.
- */
-
-/**
- * @typedef {Object} SortByWidgetOptions
- * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
- * @property {SortByIndexDefinition[]} indices Array of objects defining the different indices to choose from.
- * @property {boolean} [autoHideContainer=false] Hide the container when no results match.
- * @property {SortByWidgetCssClasses} [cssClasses] CSS classes to be added.
- * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
- */
-
-/**
- * Sort by selector is a widget used for letting the user choose between different
- * indices that contains the same data with a different order / ranking formula.
- *
- * For the users it is like they are selecting a new sort order.
- * @type {WidgetFactory}
- * @devNovel SortBySelector
- * @category sort
- * @param {SortByWidgetOptions} $0 Options for the SortBySelector widget
- * @return {Widget} Creates a new instance of the SortBySelector widget.
- * @example
- * search.addWidget(
- * instantsearch.widgets.sortBySelector({
- * container: '#sort-by-container',
- * indices: [
- * {name: 'instant_search', label: 'Most relevant'},
- * {name: 'instant_search_price_asc', label: 'Lowest price'},
- * {name: 'instant_search_price_desc', label: 'Highest price'}
- * ]
- * })
- * );
- */
-export default function sortBySelector({
- container,
- indices,
- cssClasses: userCssClasses = {},
- autoHideContainer = false,
- transformItems,
-} = {}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- // We use the same class to avoid regression on existing website. It needs to be replaced
- // eventually by `bem('select')
- select: cx(bem(null), userCssClasses.select),
- item: cx(bem('item'), userCssClasses.item),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- cssClasses,
- autoHideContainer,
- });
-
- try {
- const makeWidget = connectSortBySelector(specializedRenderer, () =>
- unmountComponentAtNode(containerNode)
- );
- return makeWidget({ indices, transformItems });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/sort-by/__tests__/__snapshots__/sort-by-test.js.snap b/src/widgets/sort-by/__tests__/__snapshots__/sort-by-test.js.snap
new file mode 100644
index 0000000000..c9695f2a12
--- /dev/null
+++ b/src/widgets/sort-by/__tests__/__snapshots__/sort-by-test.js.snap
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sortBy() calls twice ReactDOM.render( , container) 1`] = `
+
+
+
+`;
+
+exports[`sortBy() calls twice ReactDOM.render( , container) 2`] = `
+
+
+
+`;
+
+exports[`sortBy() renders transformed items 1`] = `
+
+
+
+`;
diff --git a/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js b/src/widgets/sort-by/__tests__/sort-by-test.js
similarity index 65%
rename from src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js
rename to src/widgets/sort-by/__tests__/sort-by-test.js
index 4fa34113ad..7a299dc202 100644
--- a/src/widgets/sort-by-selector/__tests__/sort-by-selector-test.js
+++ b/src/widgets/sort-by/__tests__/sort-by-test.js
@@ -1,54 +1,49 @@
-import sortBySelector from '../sort-by-selector';
-import Selector from '../../../components/Selector';
+import sortBy from '../sort-by';
+import instantSearch from '../../../lib/main';
-import instantSearch from '../../../lib/main.js';
-
-describe('sortBySelector call', () => {
+describe('sortBy call', () => {
it('throws an exception when no options', () => {
const container = document.createElement('div');
- expect(sortBySelector.bind(null, { container })).toThrow(/^Usage/);
+ expect(sortBy.bind(null, { container })).toThrow(/^Usage/);
});
- it('throws an exception when no indices', () => {
- const indices = [];
- expect(sortBySelector.bind(null, { indices })).toThrow(/^Usage/);
+ it('throws an exception when no items', () => {
+ const items = [];
+ expect(sortBy.bind(null, { items })).toThrow(/^Usage/);
});
});
-describe('sortBySelector()', () => {
+describe('sortBy()', () => {
let ReactDOM;
let container;
- let indices;
+ let items;
let cssClasses;
let widget;
let helper;
let results;
- let autoHideContainer;
beforeEach(() => {
const instantSearchInstance = instantSearch({
- apiKey: '',
- appId: '',
indexName: 'defaultIndex',
- createAlgoliaClient: () => ({}),
+ searchClient: {
+ search() {},
+ },
});
- autoHideContainer = jest.fn().mockReturnValue(Selector);
ReactDOM = { render: jest.fn() };
- sortBySelector.__Rewire__('render', ReactDOM.render);
- sortBySelector.__Rewire__('autoHideContainerHOC', autoHideContainer);
+ sortBy.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
- indices = [
- { name: 'index-a', label: 'Index A' },
- { name: 'index-b', label: 'Index B' },
+ items = [
+ { value: 'index-a', label: 'Index A' },
+ { value: 'index-b', label: 'Index B' },
];
cssClasses = {
root: ['custom-root', 'cx'],
select: 'custom-select',
item: 'custom-item',
};
- widget = sortBySelector({ container, indices, cssClasses });
+ widget = sortBy({ container, items, cssClasses });
helper = {
getIndex: jest.fn().mockReturnValue('index-a'),
setIndex: jest.fn().mockReturnThis(),
@@ -80,11 +75,11 @@ describe('sortBySelector()', () => {
});
it('renders transformed items', () => {
- widget = sortBySelector({
+ widget = sortBy({
container,
- indices,
- transformItems: items =>
- items.map(item => ({ ...item, transformed: true })),
+ items,
+ transformItems: allItems =>
+ allItems.map(item => ({ ...item, transformed: true })),
});
widget.init({ helper, instantSearchInstance: {} });
@@ -100,8 +95,8 @@ describe('sortBySelector()', () => {
});
it('should throw if there is no name attribute in a passed object', () => {
- indices.length = 0;
- indices.push({ label: 'Label without a name' });
+ items.length = 0;
+ items.push({ label: 'Label without a name' });
expect(() => {
widget.init({ helper });
}).toThrow(/Index index-a not present/);
@@ -115,7 +110,6 @@ describe('sortBySelector()', () => {
});
afterEach(() => {
- sortBySelector.__ResetDependency__('render');
- sortBySelector.__ResetDependency__('autoHideContainerHOC');
+ sortBy.__ResetDependency__('render');
});
});
diff --git a/src/widgets/sort-by/sort-by.js b/src/widgets/sort-by/sort-by.js
new file mode 100644
index 0000000000..c96d564aa7
--- /dev/null
+++ b/src/widgets/sort-by/sort-by.js
@@ -0,0 +1,113 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import Selector from '../../components/Selector/Selector.js';
+import connectSortBy from '../../connectors/sort-by/connectSortBy.js';
+import { getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('SortBy');
+
+const renderer = ({ containerNode, cssClasses }) => (
+ { currentRefinement, options, refine },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ return;
+ }
+
+ render(
+
+
+
,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+sortBy({
+ container,
+ items,
+ [cssClasses.{root, select, option}],
+ [transformItems]
+})`;
+
+/**
+ * @typedef {Object} SortByWidgetCssClasses
+ * @property {string|string[]} [root] CSS classes added to the outer ``.
+ * @property {string|string[]} [select] CSS classes added to the parent `
`.
+ * @property {string|string[]} [option] CSS classes added to each ``.
+ */
+
+/**
+ * @typedef {Object} SortByIndexDefinition
+ * @property {string} value The name of the index to target.
+ * @property {string} label The label of the index to display.
+ */
+
+/**
+ * @typedef {Object} SortByWidgetOptions
+ * @property {string|HTMLElement} container CSS Selector or HTMLElement to insert the widget.
+ * @property {SortByIndexDefinition[]} items Array of objects defining the different indices to choose from.
+ * @property {SortByWidgetCssClasses} [cssClasses] CSS classes to be added.
+ * @property {function(object[]):object[]} [transformItems] Function to transform the items passed to the templates.
+ */
+
+/**
+ * Sort by selector is a widget used for letting the user choose between different
+ * indices that contains the same data with a different order / ranking formula.
+ *
+ * For the users it is like they are selecting a new sort order.
+ * @type {WidgetFactory}
+ * @devNovel SortBy
+ * @category sort
+ * @param {SortByWidgetOptions} $0 Options for the SortBy widget
+ * @return {Widget} Creates a new instance of the SortBy widget.
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.sortBy({
+ * container: '#sort-by-container',
+ * items: [
+ * {value: 'instant_search', label: 'Most relevant'},
+ * {value: 'instant_search_price_asc', label: 'Lowest price'},
+ * {value: 'instant_search_price_desc', label: 'Highest price'}
+ * ]
+ * })
+ * );
+ */
+export default function sortBy({
+ container,
+ items,
+ cssClasses: userCssClasses = {},
+ transformItems,
+} = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ select: cx(suit({ descendantName: 'select' }), userCssClasses.select),
+ option: cx(suit({ descendantName: 'option' }), userCssClasses.option),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ });
+
+ try {
+ const makeWidget = connectSortBy(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeWidget({ items, transformItems });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/src/widgets/star-rating/__tests__/__snapshots__/star-rating-test.js.snap b/src/widgets/star-rating/__tests__/__snapshots__/star-rating-test.js.snap
deleted file mode 100644
index 4b45204d06..0000000000
--- a/src/widgets/star-rating/__tests__/__snapshots__/star-rating-test.js.snap
+++ /dev/null
@@ -1,227 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`starRating() calls twice ReactDOM.render( , container) 1`] = `
-
- {{#stars}} {{/stars}}
- {{labels.andUp}}
- {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`starRating() calls twice ReactDOM.render( , container) 2`] = `
-
- {{#stars}} {{/stars}}
- {{labels.andUp}}
- {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
diff --git a/src/widgets/star-rating/__tests__/star-rating-test.js b/src/widgets/star-rating/__tests__/star-rating-test.js
deleted file mode 100644
index 5167dfd048..0000000000
--- a/src/widgets/star-rating/__tests__/star-rating-test.js
+++ /dev/null
@@ -1,228 +0,0 @@
-import sinon from 'sinon';
-import expect from 'expect';
-
-import jsHelper from 'algoliasearch-helper';
-
-import defaultLabels from '../../../widgets/star-rating/defaultLabels.js';
-import starRating from '../star-rating.js';
-
-const SearchResults = jsHelper.SearchResults;
-
-describe('starRating()', () => {
- const attributeName = 'anAttrName';
- let ReactDOM;
- let container;
- let widget;
- let helper;
- let state;
- let createURL;
-
- let results;
-
- beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
- starRating.__Rewire__('render', ReactDOM.render);
-
- container = document.createElement('div');
- widget = starRating({
- container,
- attributeName,
- cssClasses: { body: ['body', 'cx'] },
- });
- helper = jsHelper({}, '', widget.getConfiguration({}));
- sinon.spy(helper, 'clearRefinements');
- sinon.spy(helper, 'addDisjunctiveFacetRefinement');
- sinon.spy(helper, 'getRefinements');
- helper.search = sinon.stub();
-
- state = {
- toggleRefinement: sinon.spy(),
- };
- results = {
- getFacetValues: sinon.stub().returns([]),
- hits: [],
- };
- createURL = () => '#';
- widget.init({
- helper,
- instantSearchInstance: { templatesConfig: undefined },
- });
- });
-
- it('configures the underlying disjunctive facet', () => {
- expect(widget.getConfiguration()).toEqual({
- disjunctiveFacets: ['anAttrName'],
- });
- });
-
- it('calls twice ReactDOM.render( , container)', () => {
- widget.render({ state, helper, results, createURL });
- widget.render({ state, helper, results, createURL });
-
- expect(ReactDOM.render.callCount).toBe(2);
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
- });
-
- it('hide the count==0 when there is a refinement', () => {
- helper.addDisjunctiveFacetRefinement(attributeName, 1);
- const _results = new SearchResults(helper.state, [
- {
- facets: {
- [attributeName]: { 1: 42 },
- },
- },
- {},
- ]);
-
- widget.render({ state, helper, results: _results, createURL });
- expect(ReactDOM.render.callCount).toBe(1);
- expect(ReactDOM.render.firstCall.args[0].props.facetValues).toEqual([
- {
- count: 42,
- isRefined: true,
- name: '1',
- value: '1',
- stars: [true, false, false, false, false],
- labels: defaultLabels,
- },
- ]);
- });
-
- it("doesn't call the refinement functions if not refined", () => {
- helper.getRefinements = sinon.stub().returns([]);
- widget.render({ state, helper, results, createURL });
- expect(helper.clearRefinements.called).toBe(
- false,
- 'clearRefinements never called'
- );
- expect(helper.addDisjunctiveFacetRefinement.called).toBe(
- false,
- 'addDisjunctiveFacetRefinement never called'
- );
- expect(helper.search.called).toBe(false, 'search never called');
- });
-
- it('refines the search', () => {
- helper.getRefinements = sinon.stub().returns([]);
- widget._toggleRefinement('3');
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'clearRefinements called once'
- );
- expect(helper.addDisjunctiveFacetRefinement.calledThrice).toBe(
- true,
- 'addDisjunctiveFacetRefinement called thrice'
- );
- expect(helper.search.calledOnce).toBe(true, 'search called once');
- });
-
- it('toggles the refinements', () => {
- helper.addDisjunctiveFacetRefinement(attributeName, 2);
- helper.addDisjunctiveFacetRefinement.reset();
- widget._toggleRefinement('2');
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'clearRefinements called once'
- );
- expect(helper.addDisjunctiveFacetRefinement.called).toBe(
- false,
- 'addDisjunctiveFacetRefinement never called'
- );
- expect(helper.search.calledOnce).toBe(true, 'search called once');
- });
-
- it('toggles the refinements with another facet', () => {
- helper.getRefinements = sinon.stub().returns([{ value: '2' }]);
- widget._toggleRefinement('4');
- expect(helper.clearRefinements.calledOnce).toBe(
- true,
- 'clearRefinements called once'
- );
- expect(helper.addDisjunctiveFacetRefinement.calledTwice).toBe(
- true,
- 'addDisjunctiveFacetRefinement called twice'
- );
- expect(helper.search.calledOnce).toBe(true, 'search called once');
- });
-
- it('should return the right facet counts and results', () => {
- const _widget = starRating({
- container,
- attributeName,
- cssClasses: { body: ['body', 'cx'] },
- });
- const _helper = jsHelper({}, '', _widget.getConfiguration({}));
- _helper.search = sinon.stub();
-
- _widget.init({
- helper: _helper,
- state: _helper.state,
- createURL: () => '#',
- onHistoryChange: () => {},
- instantSearchInstance: {
- templatesConfig: {},
- },
- });
-
- _widget.render({
- results: new SearchResults(_helper.state, [
- {
- facets: {
- [attributeName]: { 0: 5, 1: 10, 2: 20, 3: 50, 4: 900, 5: 100 },
- },
- },
- {},
- ]),
- state: _helper.state,
- helper: _helper,
- createURL: () => '#',
- instantSearchInstance: {
- templatesConfig: {},
- },
- });
-
- expect(ReactDOM.render.lastCall.args[0].props.facetValues).toEqual([
- {
- count: 1000,
- isRefined: false,
- labels: { andUp: '& Up' },
- name: '4',
- value: '4',
- stars: [true, true, true, true, false],
- },
- {
- count: 1050,
- isRefined: false,
- labels: { andUp: '& Up' },
- name: '3',
- value: '3',
- stars: [true, true, true, false, false],
- },
- {
- count: 1070,
- isRefined: false,
- labels: { andUp: '& Up' },
- name: '2',
- value: '2',
- stars: [true, true, false, false, false],
- },
- {
- count: 1080,
- isRefined: false,
- labels: { andUp: '& Up' },
- name: '1',
- value: '1',
- stars: [true, false, false, false, false],
- },
- ]);
- });
-
- afterEach(() => {
- starRating.__ResetDependency__('render');
- starRating.__ResetDependency__('autoHideContainerHOC');
- starRating.__ResetDependency__('headerFooterHOC');
- });
-});
diff --git a/src/widgets/star-rating/defaultLabels.js b/src/widgets/star-rating/defaultLabels.js
deleted file mode 100644
index c39bc34c59..0000000000
--- a/src/widgets/star-rating/defaultLabels.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- andUp: '& Up',
-};
diff --git a/src/widgets/star-rating/defaultTemplates.js b/src/widgets/star-rating/defaultTemplates.js
deleted file mode 100644
index df5afc04a4..0000000000
--- a/src/widgets/star-rating/defaultTemplates.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable max-len */
-export default {
- header: '',
- item: `
- {{#stars}} {{/stars}}
- {{labels.andUp}}
- {{#count}}{{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}} {{/count}}
- `,
- footer: '',
-};
diff --git a/src/widgets/star-rating/star-rating.js b/src/widgets/star-rating/star-rating.js
deleted file mode 100644
index 1587308bd5..0000000000
--- a/src/widgets/star-rating/star-rating.js
+++ /dev/null
@@ -1,200 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import RefinementList from '../../components/RefinementList/RefinementList.js';
-import connectStarRating from '../../connectors/star-rating/connectStarRating.js';
-import defaultTemplates from './defaultTemplates.js';
-import defaultLabels from './defaultLabels.js';
-
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-star-rating');
-
-const renderer = ({
- containerNode,
- cssClasses,
- templates,
- collapsible,
- transformData,
- autoHideContainer,
- renderState,
- labels,
-}) => (
- { refine, items, createURL, instantSearchInstance, hasNoResults },
- isFirstRendering
-) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- transformData,
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- const shouldAutoHideContainer = autoHideContainer && hasNoResults;
-
- render(
- ({ ...item, labels }))}
- shouldAutoHideContainer={shouldAutoHideContainer}
- templateProps={renderState.templateProps}
- toggleRefinement={refine}
- />,
- containerNode
- );
-};
-
-const usage = `Usage:
-starRating({
- container,
- attributeName,
- [ max=5 ],
- [ cssClasses.{root,header,body,footer,list,item,active,link,disabledLink,star,emptyStar,count} ],
- [ templates.{header,item,footer} ],
- [ transformData.{item} ],
- [ labels.{andUp} ],
- [ autoHideContainer=true ],
- [ collapsible=false ]
-})`;
-
-/**
- * @typedef {Object} StarWidgetLabels
- * @property {string} [andUp] Label used to suffix the ratings.
- */
-
-/**
- * @typedef {Object} StarWidgetTemplates
- * @property {string|function} [header] Header template.
- * @property {string|function} [item] Item template, provided with `name`, `count`, `isRefined`, `url` data properties.
- * @property {string|function} [footer] Footer template.
- */
-
-/**
- * @typedef {Object} StarWidgetCssClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- * @property {string|string[]} [list] CSS class to add to the list element.
- * @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [link] CSS class to add to each link element.
- * @property {string|string[]} [disabledLink] CSS class to add to each disabled link (when using the default template).
- * @property {string|string[]} [count] CSS class to add to each counters
- * @property {string|string[]} [star] CSS class to add to each star element (when using the default template).
- * @property {string|string[]} [emptyStar] CSS class to add to each empty star element (when using the default template).
- * @property {string|string[]} [active] CSS class to add to each active element.
- */
-
-/**
- * @typedef {Object} StarWidgetCollapsibleOption
- * @property {boolean} collapsed If set to true, the widget will be collapsed at first rendering.
- */
-
-/**
- * @typedef {Object} StarWidgetTransforms
- * @property {function} [item] Function to change the object passed to the `item` template.
- */
-
-/**
- * @typedef {Object} StarWidgetOptions
- * @property {string|HTMLElement} container Place where to insert the widget in your webpage.
- * @property {string} attributeName Name of the attribute in your records that contains the ratings.
- * @property {number} [max=5] The maximum rating value.
- * @property {StarWidgetLabels} [labels] Labels used by the default template.
- * @property {StarWidgetTemplates} [templates] Templates to use for the widget.
- * @property {StarWidgetTransforms} [transformData] Object that contains the functions to be applied on the data * before being used for templating. Valid keys are `body` for the body template.
- * @property {boolean} [autoHideContainer=true] Make the widget hides itself when there is no results matching.
- * @property {StarWidgetCssClasses} [cssClasses] CSS classes to add.
- * @property {boolean|StarWidgetCollapsibleOption} [collapsible=false] If set to true, the widget can be collapsed. This parameter can also be
- */
-
-/**
- * Star rating is used for displaying grade like filters. The values are normalized within boundaries.
- *
- * The maximum value can be set (with `max`), the minimum is always 0.
- *
- * @requirements
- * The attribute passed to `attributeName` must be declared as an
- * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
- * in your Algolia settings.
- *
- * The values inside this attribute must be JavaScript numbers (not strings).
- *
- * @type {WidgetFactory}
- * @devNovel StarRating
- * @category filter
- * @param {StarWidgetOptions} $0 StarRating widget options.
- * @return {Widget} A new StarRating widget instance.
- * @example
- * search.addWidget(
- * instantsearch.widgets.starRating({
- * container: '#stars',
- * attributeName: 'rating',
- * max: 5,
- * labels: {
- * andUp: '& Up'
- * }
- * })
- * );
- */
-export default function starRating({
- container,
- attributeName,
- max = 5,
- cssClasses: userCssClasses = {},
- labels = defaultLabels,
- templates = defaultTemplates,
- collapsible = false,
- transformData,
- autoHideContainer = true,
-} = {}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- link: cx(bem('link'), userCssClasses.link),
- disabledLink: cx(bem('link', 'disabled'), userCssClasses.disabledLink),
- count: cx(bem('count'), userCssClasses.count),
- star: cx(bem('star'), userCssClasses.star),
- emptyStar: cx(bem('star', 'empty'), userCssClasses.emptyStar),
- active: cx(bem('item', 'active'), userCssClasses.active),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState: {},
- templates,
- transformData,
- labels,
- });
-
- try {
- const makeWidget = connectStarRating(specializedRenderer, () =>
- unmountComponentAtNode(containerNode)
- );
- return makeWidget({ attributeName, max });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/stats/__tests__/__snapshots__/stats-test.js.snap b/src/widgets/stats/__tests__/__snapshots__/stats-test.js.snap
index fb4546e094..d46055f42d 100644
--- a/src/widgets/stats/__tests__/__snapshots__/stats-test.js.snap
+++ b/src/widgets/stats/__tests__/__snapshots__/stats-test.js.snap
@@ -1,15 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`stats() calls twice ReactDOM.render( , container) 1`] = `
- , container) 1`] = `
page={0}
processingTimeMS={42}
query="a query"
- shouldAutoHideContainer={false}
templateProps={
Object {
"templates": Object {
- "body": "{{#hasNoResults}}No results{{/hasNoResults}}
- {{#hasOneResult}}1 result{{/hasOneResult}}
- {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
- found in {{processingTimeMS}}ms ",
- "footer": "",
- "header": "",
+ "text": "{{#hasNoResults}}No results{{/hasNoResults}}
+ {{#hasOneResult}}1 result{{/hasOneResult}}
+ {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "body": false,
- "footer": false,
- "header": false,
+ "text": false,
},
}
}
@@ -42,15 +31,11 @@ exports[`stats() calls twice ReactDOM.render( , container) 1`] = `
`;
exports[`stats() calls twice ReactDOM.render( , container) 2`] = `
- , container) 2`] = `
page={0}
processingTimeMS={42}
query="a query"
- shouldAutoHideContainer={false}
templateProps={
Object {
"templates": Object {
- "body": "{{#hasNoResults}}No results{{/hasNoResults}}
- {{#hasOneResult}}1 result{{/hasOneResult}}
- {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
- found in {{processingTimeMS}}ms ",
- "footer": "",
- "header": "",
+ "text": "{{#hasNoResults}}No results{{/hasNoResults}}
+ {{#hasOneResult}}1 result{{/hasOneResult}}
+ {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms",
},
"templatesConfig": undefined,
- "transformData": undefined,
"useCustomCompileOptions": Object {
- "body": false,
- "footer": false,
- "header": false,
+ "text": false,
},
}
}
diff --git a/src/widgets/stats/__tests__/stats-test.js b/src/widgets/stats/__tests__/stats-test.js
index 85bc8afb01..72191397aa 100644
--- a/src/widgets/stats/__tests__/stats-test.js
+++ b/src/widgets/stats/__tests__/stats-test.js
@@ -1,5 +1,3 @@
-import expect from 'expect';
-import sinon from 'sinon';
import stats from '../stats';
const instantSearchInstance = { templatesConfig: undefined };
@@ -17,11 +15,11 @@ describe('stats()', () => {
let results;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
stats.__Rewire__('render', ReactDOM.render);
container = document.createElement('div');
- widget = stats({ container, cssClasses: { body: ['body', 'cx'] } });
+ widget = stats({ container, cssClasses: { text: ['text', 'cx'] } });
results = {
hits: [{}, {}],
nbHits: 20,
@@ -45,19 +43,14 @@ describe('stats()', () => {
it('calls twice ReactDOM.render( , container)', () => {
widget.render({ results, instantSearchInstance });
widget.render({ results, instantSearchInstance });
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[1]).toEqual(container);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(container);
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(container);
});
afterEach(() => {
stats.__ResetDependency__('render');
- stats.__ResetDependency__('autoHideContainerHOC');
- stats.__ResetDependency__('headerFooterHOC');
});
});
diff --git a/src/widgets/stats/defaultTemplates.js b/src/widgets/stats/defaultTemplates.js
index 556b7560c8..333ad9b5b8 100644
--- a/src/widgets/stats/defaultTemplates.js
+++ b/src/widgets/stats/defaultTemplates.js
@@ -1,8 +1,5 @@
export default {
- header: '',
- body: `{{#hasNoResults}}No results{{/hasNoResults}}
- {{#hasOneResult}}1 result{{/hasOneResult}}
- {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}}
- found in {{processingTimeMS}}ms `,
- footer: '',
+ text: `{{#hasNoResults}}No results{{/hasNoResults}}
+ {{#hasOneResult}}1 result{{/hasOneResult}}
+ {{#hasManyResults}}{{#helpers.formatNumber}}{{nbHits}}{{/helpers.formatNumber}} results{{/hasManyResults}} found in {{processingTimeMS}}ms`,
};
diff --git a/src/widgets/stats/stats.js b/src/widgets/stats/stats.js
index 30203d75b2..6c5eb797fa 100644
--- a/src/widgets/stats/stats.js
+++ b/src/widgets/stats/stats.js
@@ -1,27 +1,14 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
-
import Stats from '../../components/Stats/Stats.js';
import connectStats from '../../connectors/stats/connectStats.js';
import defaultTemplates from './defaultTemplates.js';
+import { prepareTemplateProps, getContainerNode } from '../../lib/utils.js';
+import { component } from '../../lib/suit';
-import {
- bemHelper,
- prepareTemplateProps,
- getContainerNode,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-stats');
+const suit = component('Stats');
-const renderer = ({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState,
- templates,
- transformData,
-}) => (
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
{
hitsPerPage,
nbHits,
@@ -35,19 +22,16 @@ const renderer = ({
) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
- transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
});
+
return;
}
- const shouldAutoHideContainer = autoHideContainer && nbHits === 0;
-
render(
,
containerNode
@@ -65,36 +48,24 @@ const renderer = ({
const usage = `Usage:
stats({
container,
- [ templates.{header, body, footer} ],
- [ transformData.{body} ],
- [ autoHideContainer=true ],
- [ cssClasses.{root, header, body, footer, time} ],
+ [ templates.{text} ],
+ [ cssClasses.{root, text} ],
})`;
/**
* @typedef {Object} StatsWidgetTemplates
- * @property {string|function} [header=''] Header template.
- * @property {string|function} [body] Body template, provided with `hasManyResults`,
+ * @property {string|function} [text] Text template, provided with `hasManyResults`,
* `hasNoResults`, `hasOneResult`, `hitsPerPage`, `nbHits`, `nbPages`, `page`, `processingTimeMS`, `query`.
- * @property {string|function} [footer=''] Footer template.
*/
/**
* @typedef {Object} StatsWidgetCssClasses
* @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- * @property {string|string[]} [time] CSS class to add to the element wrapping the time processingTimeMs.
- */
-
-/**
- * @typedef {Object} StatsWidgetTransforms
- * @property {function(StatsBodyData):object} [body] Updates the content of object passed to the `body` template.
+ * @property {string|string[]} [text] CSS class to add to the text span element.
*/
/**
- * @typedef {Object} StatsBodyData
+ * @typedef {Object} StatsTextData
* @property {boolean} hasManyResults True if the result set has more than one result.
* @property {boolean} hasNoResults True if the result set has no result.
* @property {boolean} hasOneResult True if the result set has exactly one result.
@@ -110,8 +81,6 @@ stats({
* @typedef {Object} StatsWidgetOptions
* @property {string|HTMLElement} container Place where to insert the widget in your webpage.
* @property {StatsWidgetTemplates} [templates] Templates to use for the widget.
- * @property {StatsWidgetTransforms} [transformData] Object that contains the functions to be applied on the data * before being used for templating. Valid keys are `body` for the body template.
- * @property {boolean} [autoHideContainer=true] Make the widget hides itself when there is no results matching.
* @property {StatsWidgetCssClasses} [cssClasses] CSS classes to add.
*/
@@ -135,9 +104,6 @@ stats({
export default function stats({
container,
cssClasses: userCssClasses = {},
- autoHideContainer = true,
- collapsible = false,
- transformData,
templates = defaultTemplates,
} = {}) {
if (!container) {
@@ -147,21 +113,15 @@ export default function stats({
const containerNode = getContainerNode(container);
const cssClasses = {
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- header: cx(bem('header'), userCssClasses.header),
- root: cx(bem(null), userCssClasses.root),
- time: cx(bem('time'), userCssClasses.time),
+ root: cx(suit(), userCssClasses.root),
+ text: cx(suit({ descendantName: 'text' }), userCssClasses.text),
};
const specializedRenderer = renderer({
containerNode,
cssClasses,
- collapsible,
- autoHideContainer,
renderState: {},
templates,
- transformData,
});
try {
@@ -169,7 +129,7 @@ export default function stats({
unmountComponentAtNode(containerNode)
);
return makeWidget();
- } catch (e) {
+ } catch (error) {
throw new Error(usage);
}
}
diff --git a/src/widgets/toggle/__tests__/__snapshots__/currentToggle-test.js.snap b/src/widgets/toggle/__tests__/__snapshots__/currentToggle-test.js.snap
deleted file mode 100644
index a51e621f88..0000000000
--- a/src/widgets/toggle/__tests__/__snapshots__/currentToggle-test.js.snap
+++ /dev/null
@@ -1,550 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`currentToggle() good usage render supports negative numeric off or on values 1`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render supports negative numeric off or on values 2`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render understands cssClasses 1`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render when refined 1`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render when refined 2`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render with facet values 1`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render with facet values 2`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render without facet values 1`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
-
-exports[`currentToggle() good usage render without facet values 2`] = `
-
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
-",
- },
- "templatesConfig": undefined,
- "transformData": undefined,
- "useCustomCompileOptions": Object {
- "footer": false,
- "header": false,
- "item": false,
- },
- }
- }
- toggleRefinement={[Function]}
-/>
-`;
diff --git a/src/widgets/toggle/defaultTemplates.js b/src/widgets/toggle/defaultTemplates.js
deleted file mode 100644
index fa9da93e2a..0000000000
--- a/src/widgets/toggle/defaultTemplates.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
- header: '',
- item: `
- {{name}}
- {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
- `,
- footer: '',
-};
diff --git a/src/widgets/toggle/toggle.js b/src/widgets/toggle/toggle.js
deleted file mode 100644
index 806cbc74a2..0000000000
--- a/src/widgets/toggle/toggle.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, { render, unmountComponentAtNode } from 'preact-compat';
-import cx from 'classnames';
-
-import defaultTemplates from './defaultTemplates.js';
-import RefinementList from '../../components/RefinementList/RefinementList.js';
-import connectToggle from '../../connectors/toggle/connectToggle.js';
-
-import {
- bemHelper,
- getContainerNode,
- prepareTemplateProps,
-} from '../../lib/utils.js';
-
-const bem = bemHelper('ais-toggle');
-
-const renderer = ({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState,
- templates,
- transformData,
-}) => (
- { value, createURL, refine, instantSearchInstance },
- isFirstRendering
-) => {
- if (isFirstRendering) {
- renderState.templateProps = prepareTemplateProps({
- transformData,
- defaultTemplates,
- templatesConfig: instantSearchInstance.templatesConfig,
- templates,
- });
- return;
- }
-
- const shouldAutoHideContainer =
- autoHideContainer && (value.count === 0 || value.count === null);
-
- render(
- refine({ isRefined })}
- />,
- containerNode
- );
-};
-
-const usage = `Usage:
-toggle({
- container,
- attributeName,
- label,
- [ values={on: true, off: undefined} ],
- [ cssClasses.{root,header,body,footer,list,item,active,label,checkbox,count} ],
- [ templates.{header,item,footer} ],
- [ transformData.{item} ],
- [ autoHideContainer=true ],
- [ collapsible=false ]
-})`;
-
-/**
- * @typedef {Object} ToggleWidgetCSSClasses
- * @property {string|string[]} [root] CSS class to add to the root element.
- * @property {string|string[]} [header] CSS class to add to the header element.
- * @property {string|string[]} [body] CSS class to add to the body element.
- * @property {string|string[]} [footer] CSS class to add to the footer element.
- * @property {string|string[]} [list] CSS class to add to the list element.
- * @property {string|string[]} [item] CSS class to add to each item element.
- * @property {string|string[]} [active] CSS class to add to each active element.
- * @property {string|string[]} [label] CSS class to add to each
- * label element (when using the default template).
- * @property {string|string[]} [checkbox] CSS class to add to each
- * checkbox element (when using the default template).
- * @property {string|string[]} [count] CSS class to add to each count.
- */
-
-/**
- * @typedef {Object} ToggleWidgetTransforms
- * @property {function(Object):Object} item Function to change the object passed to the `item`. template
- */
-
-/**
- * @typedef {Object} ToggleWidgetTemplates
- * @property {string|function} header Header template.
- * @property {string|function} item Item template, provided with `name`, `count`, `isRefined`, `url` data properties.
- * count is always the number of hits that would be shown if you toggle the widget. We also provide
- * `onFacetValue` and `offFacetValue` objects with according counts.
- * @property {string|function} footer Footer template.
- */
-
-/**
- * @typedef {Object} ToggleWidgetValues
- * @property {string|number|boolean} on Value to filter on when checked.
- * @property {string|number|boolean} off Value to filter on when unchecked.
- * element (when using the default template). By default when switching to `off`, no refinement will be asked. So you
- * will get both `true` and `false` results. If you set the off value to `false` then you will get only objects
- * having `false` has a value for the selected attribute.
- */
-
-/**
- * @typedef {Object} ToggleWidgetCollapsibleOption
- * @property {boolean} collapsed If set to true, the widget will be collapsed at first rendering.
- */
-
-/**
- * @typedef {Object} ToggleWidgetOptions
- * @property {string|HTMLElement} container Place where to insert the widget in your webpage.
- * @property {string} attributeName Name of the attribute for faceting (eg. "free_shipping").
- * @property {string} label Human-readable name of the filter (eg. "Free Shipping").
- * @property {ToggleWidgetValues} [values={on: true, off: undefined}] Values that the widget can set.
- * @property {ToggleWidgetTemplates} [templates] Templates to use for the widget.
- * @property {ToggleWidgetTransforms} [transformData] Object that contains the functions to be applied on the data * before being used for templating. Valid keys are `body` for the body template.
- * @property {boolean} [autoHideContainer=true] Make the widget hides itself when there is no results matching.
- * @property {ToggleWidgetCSSClasses} [cssClasses] CSS classes to add.
- * @property {boolean|ToggleWidgetCollapsibleOption} collapsible If set to true, the widget can be collapsed. This parameter can also be
- * an object, with the property collapsed, if you want the toggle to be collapsed initially.
- */
-
-/**
- * The toggle widget lets the user either:
- * - switch between two values for a single facetted attribute (free_shipping / not_free_shipping)
- * - toggle a faceted value on and off (only 'canon' for brands)
- *
- * This widget is particularly useful if you have a boolean value in the records.
- *
- * @requirements
- * The attribute passed to `attributeName` must be declared as an
- * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
- * in your Algolia settings.
- *
- * @type {WidgetFactory}
- * @devNovel Toggle
- * @category filter
- * @param {ToggleWidgetOptions} $0 Options for the Toggle widget.
- * @return {Widget} A new instance of the Toggle widget
- * @example
- * search.addWidget(
- * instantsearch.widgets.toggle({
- * container: '#free-shipping',
- * attributeName: 'free_shipping',
- * label: 'Free Shipping',
- * values: {
- * on: true,
- * },
- * templates: {
- * header: 'Shipping'
- * }
- * })
- * );
- */
-export default function toggle({
- container,
- attributeName,
- label,
- cssClasses: userCssClasses = {},
- templates = defaultTemplates,
- transformData,
- autoHideContainer = true,
- collapsible = false,
- values: userValues = { on: true, off: undefined },
-} = {}) {
- if (!container) {
- throw new Error(usage);
- }
-
- const containerNode = getContainerNode(container);
-
- const cssClasses = {
- root: cx(bem(null), userCssClasses.root),
- header: cx(bem('header'), userCssClasses.header),
- body: cx(bem('body'), userCssClasses.body),
- footer: cx(bem('footer'), userCssClasses.footer),
- list: cx(bem('list'), userCssClasses.list),
- item: cx(bem('item'), userCssClasses.item),
- active: cx(bem('item', 'active'), userCssClasses.active),
- label: cx(bem('label'), userCssClasses.label),
- checkbox: cx(bem('checkbox'), userCssClasses.checkbox),
- count: cx(bem('count'), userCssClasses.count),
- };
-
- const specializedRenderer = renderer({
- containerNode,
- cssClasses,
- collapsible,
- autoHideContainer,
- renderState: {},
- templates,
- transformData,
- });
-
- try {
- const makeWidget = connectToggle(specializedRenderer, () =>
- unmountComponentAtNode(containerNode)
- );
- return makeWidget({ attributeName, label, values: userValues });
- } catch (e) {
- throw new Error(usage);
- }
-}
diff --git a/src/widgets/toggleRefinement/__tests__/__snapshots__/currentToggle-test.js.snap b/src/widgets/toggleRefinement/__tests__/__snapshots__/currentToggle-test.js.snap
new file mode 100644
index 0000000000..322783979b
--- /dev/null
+++ b/src/widgets/toggleRefinement/__tests__/__snapshots__/currentToggle-test.js.snap
@@ -0,0 +1,370 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`currentToggle() good usage render supports negative numeric off or on values 1`] = `
+
+`;
+
+exports[`currentToggle() good usage render supports negative numeric off or on values 2`] = `
+
+`;
+
+exports[`currentToggle() good usage render understands cssClasses 1`] = `
+
+`;
+
+exports[`currentToggle() good usage render when refined 1`] = `
+
+`;
+
+exports[`currentToggle() good usage render when refined 2`] = `
+
+`;
+
+exports[`currentToggle() good usage render with facet values 1`] = `
+
+`;
+
+exports[`currentToggle() good usage render with facet values 2`] = `
+
+`;
+
+exports[`currentToggle() good usage render without facet values 1`] = `
+
+`;
+
+exports[`currentToggle() good usage render without facet values 2`] = `
+
+`;
diff --git a/src/widgets/toggle/__tests__/currentToggle-test.js b/src/widgets/toggleRefinement/__tests__/currentToggle-test.js
similarity index 50%
rename from src/widgets/toggle/__tests__/currentToggle-test.js
rename to src/widgets/toggleRefinement/__tests__/currentToggle-test.js
index db49d2bfcb..69d1a612d3 100644
--- a/src/widgets/toggle/__tests__/currentToggle-test.js
+++ b/src/widgets/toggleRefinement/__tests__/currentToggle-test.js
@@ -1,7 +1,4 @@
-import expect from 'expect';
-import sinon from 'sinon';
-import currentToggle from '../toggle.js';
-import defaultTemplates from '../defaultTemplates.js';
+import currentToggle from '../toggleRefinement.js';
import RefinementList from '../../../components/RefinementList/RefinementList.js';
import jsHelper from 'algoliasearch-helper';
@@ -11,39 +8,19 @@ describe('currentToggle()', () => {
let ReactDOM;
let containerNode;
let widget;
- let attributeName;
- let label;
- let userValues;
- let collapsible;
- let cssClasses;
+ let attribute;
let instantSearchInstance;
beforeEach(() => {
- ReactDOM = { render: sinon.spy() };
+ ReactDOM = { render: jest.fn() };
currentToggle.__Rewire__('render', ReactDOM.render);
containerNode = document.createElement('div');
- label = 'Hello, ';
- attributeName = 'world!';
- cssClasses = {
- active: 'ais-toggle--item__active',
- body: 'ais-toggle--body',
- checkbox: 'ais-toggle--checkbox',
- count: 'ais-toggle--count',
- footer: 'ais-toggle--footer',
- header: 'ais-toggle--header',
- item: 'ais-toggle--item',
- label: 'ais-toggle--label',
- list: 'ais-toggle--list',
- root: 'ais-toggle',
- };
- collapsible = false;
- userValues = { on: true, off: undefined };
+ attribute = 'world!';
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
+ attribute,
});
instantSearchInstance = { templatesConfig: undefined };
});
@@ -55,43 +32,24 @@ describe('currentToggle()', () => {
});
describe('render', () => {
- let templateProps;
let results;
let helper;
let state;
- let props;
let createURL;
beforeEach(() => {
- templateProps = {
- templatesConfig: undefined,
- templates: defaultTemplates,
- useCustomCompileOptions: {
- header: false,
- item: false,
- footer: false,
- },
- transformData: undefined,
- };
helper = {
state: {
- isDisjunctiveFacetRefined: sinon.stub().returns(false),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(false),
},
- removeDisjunctiveFacetRefinement: sinon.spy(),
- addDisjunctiveFacetRefinement: sinon.spy(),
- search: sinon.spy(),
+ removeDisjunctiveFacetRefinement: jest.fn(),
+ addDisjunctiveFacetRefinement: jest.fn(),
+ search: jest.fn(),
};
state = {
- removeDisjunctiveFacetRefinement: sinon.spy(),
- addDisjunctiveFacetRefinement: sinon.spy(),
- isDisjunctiveFacetRefined: sinon.stub().returns(false),
- };
- props = {
- cssClasses,
- collapsible: false,
- templateProps,
- createURL() {},
- toggleRefinement() {},
+ removeDisjunctiveFacetRefinement: jest.fn(),
+ addDisjunctiveFacetRefinement: jest.fn(),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(false),
};
createURL = () => '#';
widget.init({ state, helper, createURL, instantSearchInstance });
@@ -101,118 +59,89 @@ describe('currentToggle()', () => {
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([{ name: 'true', count: 2 }, { name: 'false', count: 1 }]),
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
+ { name: 'true', count: 2 },
+ { name: 'false', count: 1 },
+ ]),
};
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
});
widget.getConfiguration();
widget.init({ helper, state, createURL, instantSearchInstance });
widget.render({ results, helper, state });
widget.render({ results, helper, state });
- expect(ReactDOM.render.calledTwice).toBe(
- true,
- 'ReactDOM.render called twice'
- );
- expect(ReactDOM.render.firstCall.args[1]).toEqual(containerNode);
- expect(ReactDOM.render.secondCall.args[1]).toEqual(containerNode);
+ expect(ReactDOM.render).toHaveBeenCalledTimes(2);
+ expect(ReactDOM.render.mock.calls[0][1]).toEqual(containerNode);
+ expect(ReactDOM.render.mock.calls[1][1]).toEqual(containerNode);
});
it('understands cssClasses', () => {
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
{ name: 'true', count: 2, isRefined: false },
{ name: 'false', count: 1, isRefined: false },
]),
};
- props.cssClasses.root = 'ais-toggle test';
- props = {
- facetValues: [
- {
- count: 2,
- isRefined: false,
- name: label,
- offFacetValue: { count: 3, name: 'Hello, ', isRefined: false },
- onFacetValue: { count: 2, name: 'Hello, ', isRefined: false },
- },
- ],
- shouldAutoHideContainer: false,
- ...props,
- };
- cssClasses = props.cssClasses;
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- cssClasses: { root: 'test' },
- userValues,
+ attribute,
+ cssClasses: {
+ root: 'test',
+ label: 'test-label',
+ labelText: 'test-labelText',
+ checkbox: 'test-checkbox',
+ },
+ /* on: true, off: undefined */
RefinementList,
- collapsible,
});
widget.getConfiguration();
widget.init({ state, helper, createURL, instantSearchInstance });
widget.render({ results, helper, state });
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
});
it('with facet values', () => {
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
{ name: 'true', count: 2, isRefined: false },
{ name: 'false', count: 1, isRefined: false },
]),
};
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
RefinementList,
- collapsible,
});
widget.getConfiguration();
widget.init({ state, helper, createURL, instantSearchInstance });
widget.render({ results, helper, state });
widget.render({ results, helper, state });
- props = {
- facetValues: [
- {
- count: 2,
- isRefined: false,
- name: label,
- offFacetValue: { count: 3, name: 'Hello, ', isRefined: false },
- onFacetValue: { count: 2, name: 'Hello, ', isRefined: false },
- },
- ],
- shouldAutoHideContainer: false,
- ...props,
- };
-
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
});
it('supports negative numeric off or on values', () => {
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
{ name: '-2', count: 2, isRefined: true },
{ name: '5', count: 1, isRefined: false },
]),
@@ -220,13 +149,9 @@ describe('currentToggle()', () => {
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: {
- off: -2,
- on: 5,
- },
- collapsible,
+ attribute,
+ off: -2,
+ on: 5,
});
const config = widget.getConfiguration();
@@ -242,31 +167,17 @@ describe('currentToggle()', () => {
widget.render({ results, helper: altHelper, state });
widget.render({ results, helper: altHelper, state });
- props = {
- facetValues: [
- {
- count: 1,
- isRefined: false,
- name: label,
- offFacetValue: { count: 2, name: label, isRefined: true },
- onFacetValue: { count: 1, name: label, isRefined: false },
- },
- ],
- shouldAutoHideContainer: false,
- ...props,
- };
-
// The first call is not the one expected, because of the new init rendering..
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
widget.toggleRefinement({ isRefined: true });
+ expect(altHelper.state.isDisjunctiveFacetRefined(attribute, 5)).toBe(
+ false
+ );
expect(
- altHelper.state.isDisjunctiveFacetRefined(attributeName, 5)
- ).toBe(false);
- expect(
- altHelper.state.isDisjunctiveFacetRefined(attributeName, '\\-2')
+ altHelper.state.isDisjunctiveFacetRefined(attribute, '\\-2')
).toBe(true);
});
@@ -274,117 +185,83 @@ describe('currentToggle()', () => {
results = {
hits: [],
nbHits: 0,
- getFacetValues: sinon.stub().returns([]),
+ getFacetValues: jest.fn().mockReturnValue([]),
};
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
RefinementList,
- collapsible,
});
widget.getConfiguration();
widget.init({ state, helper, createURL, instantSearchInstance });
widget.render({ results, helper, state });
widget.render({ results, helper, state });
- props = {
- facetValues: [
- {
- name: label,
- isRefined: false,
- count: null,
- onFacetValue: { name: label, isRefined: false, count: null },
- offFacetValue: { name: label, isRefined: false, count: 0 },
- },
- ],
- shouldAutoHideContainer: true,
- ...props,
- };
-
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
});
it('when refined', () => {
helper = {
state: {
- isDisjunctiveFacetRefined: sinon.stub().returns(true),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(true),
},
};
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
{ name: 'true', count: 2, isRefined: true },
{ name: 'false', count: 1, isRefined: false },
]),
};
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
RefinementList,
- collapsible,
});
widget.getConfiguration();
widget.init({ state, helper, createURL, instantSearchInstance });
widget.render({ results, helper, state });
widget.render({ results, helper, state });
- props = {
- facetValues: [
- {
- count: 3,
- isRefined: true,
- name: label,
- onFacetValue: { name: label, isRefined: true, count: 2 },
- offFacetValue: { name: label, isRefined: false, count: 3 },
- },
- ],
- shouldAutoHideContainer: false,
- ...props,
- };
-
- expect(ReactDOM.render.firstCall.args[0]).toMatchSnapshot();
- expect(ReactDOM.render.secondCall.args[0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[0][0]).toMatchSnapshot();
+ expect(ReactDOM.render.mock.calls[1][0]).toMatchSnapshot();
});
it('using props.refine', () => {
results = {
hits: [{ Hello: ', world!' }],
nbHits: 1,
- getFacetValues: sinon
- .stub()
- .returns([{ name: 'true', count: 2 }, { name: 'false', count: 1 }]),
+ getFacetValues: jest
+ .fn()
+ .mockReturnValue([
+ { name: 'true', count: 2 },
+ { name: 'false', count: 1 },
+ ]),
};
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- cssClasses,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
RefinementList,
- collapsible,
});
widget.getConfiguration();
widget.init({ state, helper, createURL, instantSearchInstance });
widget.render({ results, helper, state });
- const { toggleRefinement } = ReactDOM.render.firstCall.args[0].props;
- expect(typeof toggleRefinement).toEqual('function');
- toggleRefinement();
- expect(helper.addDisjunctiveFacetRefinement.calledOnce).toBe(true);
- expect(
- helper.addDisjunctiveFacetRefinement.calledWithExactly(
- attributeName,
- true
- )
- ).toBe(true);
- helper.hasRefinements = sinon.stub().returns(true);
+ const { refine } = ReactDOM.render.mock.calls[0][0].props;
+ expect(typeof refine).toEqual('function');
+ refine();
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledTimes(1);
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledWith(
+ attribute,
+ true
+ );
+ helper.hasRefinements = jest.fn().mockReturnValue(true);
});
});
@@ -400,9 +277,9 @@ describe('currentToggle()', () => {
beforeEach(() => {
helper = {
- removeDisjunctiveFacetRefinement: sinon.spy(),
- addDisjunctiveFacetRefinement: sinon.spy(),
- search: sinon.spy(),
+ removeDisjunctiveFacetRefinement: jest.fn(),
+ addDisjunctiveFacetRefinement: jest.fn(),
+ search: jest.fn(),
};
});
@@ -411,13 +288,12 @@ describe('currentToggle()', () => {
// Given
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
});
widget.getConfiguration();
const state = {
- isDisjunctiveFacetRefined: sinon.stub().returns(false),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(false),
};
const createURL = () => '#';
widget.init({ state, helper, createURL, instantSearchInstance });
@@ -426,22 +302,24 @@ describe('currentToggle()', () => {
toggleOn();
// Then
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledWith(
+ attribute,
+ true
+ );
expect(
- helper.addDisjunctiveFacetRefinement.calledWith(attributeName, true)
- ).toBe(true);
- expect(helper.removeDisjunctiveFacetRefinement.called).toBe(false);
+ helper.removeDisjunctiveFacetRefinement
+ ).not.toHaveBeenCalled();
});
it('toggle off should remove all filters', () => {
// Given
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- userValues,
+ attribute,
+ /* on: true, off: undefined */
});
widget.getConfiguration();
const state = {
- isDisjunctiveFacetRefined: sinon.stub().returns(true),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(true),
};
const createURL = () => '#';
widget.init({ state, helper, createURL, instantSearchInstance });
@@ -450,24 +328,21 @@ describe('currentToggle()', () => {
toggleOff();
// Then
- expect(
- helper.removeDisjunctiveFacetRefinement.calledWith(
- attributeName,
- true
- )
- ).toBe(true);
- expect(helper.addDisjunctiveFacetRefinement.called).toBe(false);
+ expect(helper.removeDisjunctiveFacetRefinement).toHaveBeenCalledWith(
+ attribute,
+ true
+ );
+ expect(helper.addDisjunctiveFacetRefinement).not.toHaveBeenCalled();
});
});
describe('specific values', () => {
it('toggle on should change the refined value', () => {
// Given
- userValues = { on: 'on', off: 'off' };
+ const userValues = { on: 'on', off: 'off' };
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: userValues,
+ attribute,
+ ...userValues,
});
const config = widget.getConfiguration();
@@ -488,25 +363,24 @@ describe('currentToggle()', () => {
// Then
expect(
- altHelper.state.isDisjunctiveFacetRefined(attributeName, 'off')
+ altHelper.state.isDisjunctiveFacetRefined(attribute, 'off')
).toBe(false);
expect(
- altHelper.state.isDisjunctiveFacetRefined(attributeName, 'on')
+ altHelper.state.isDisjunctiveFacetRefined(attribute, 'on')
).toBe(true);
});
it('toggle off should change the refined value', () => {
// Given
- userValues = { on: 'on', off: 'off' };
+ const userValues = { on: 'on', off: 'off' };
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: userValues,
+ attribute,
+ ...userValues,
});
widget.getConfiguration();
const state = {
- isDisjunctiveFacetRefined: sinon.stub().returns(true),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(true),
};
const createURL = () => '#';
widget.init({ state, helper, createURL, instantSearchInstance });
@@ -515,18 +389,14 @@ describe('currentToggle()', () => {
toggleOff();
// Then
- expect(
- helper.removeDisjunctiveFacetRefinement.calledWith(
- attributeName,
- 'on'
- )
- ).toBe(true);
- expect(
- helper.addDisjunctiveFacetRefinement.calledWith(
- attributeName,
- 'off'
- )
- ).toBe(true);
+ expect(helper.removeDisjunctiveFacetRefinement).toHaveBeenCalledWith(
+ attribute,
+ 'on'
+ );
+ expect(helper.addDisjunctiveFacetRefinement).toHaveBeenCalledWith(
+ attribute,
+ 'off'
+ );
});
});
});
@@ -535,12 +405,11 @@ describe('currentToggle()', () => {
const createURL = () => '#';
it('should add a refinement for custom off value on init', () => {
// Given
- userValues = { on: 'on', off: 'off' };
+ const userValues = { on: 'on', off: 'off' };
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: userValues,
+ attribute,
+ ...userValues,
});
const config = widget.getConfiguration();
const helper = jsHelper({}, '', config);
@@ -554,56 +423,55 @@ describe('currentToggle()', () => {
});
// Then
- expect(
- helper.state.isDisjunctiveFacetRefined(attributeName, 'off')
- ).toBe(true);
+ expect(helper.state.isDisjunctiveFacetRefined(attribute, 'off')).toBe(
+ true
+ );
});
it('should not add a refinement for custom off value on init if already checked', () => {
// Given
- userValues = { on: 'on', off: 'off' };
+ const userValues = { on: 'on', off: 'off' };
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: userValues,
+ attribute,
+ ...userValues,
});
widget.getConfiguration();
const state = {
- isDisjunctiveFacetRefined: sinon.stub().returns(true),
+ isDisjunctiveFacetRefined: jest.fn().mockReturnValue(true),
};
const helper = {
- addDisjunctiveFacetRefinement: sinon.spy(),
+ addDisjunctiveFacetRefinement: jest.fn(),
};
// When
widget.init({ state, helper, createURL, instantSearchInstance });
// Then
- expect(helper.addDisjunctiveFacetRefinement.called).toBe(false);
+ expect(helper.addDisjunctiveFacetRefinement).not.toHaveBeenCalled();
});
it('should not add a refinement for no custom off value on init', () => {
// Given
+ const userValues = { on: 'on' };
widget = currentToggle({
container: containerNode,
- attributeName,
- label,
- values: userValues,
+ attribute,
+ ...userValues,
});
widget.getConfiguration();
const state = {
isDisjunctiveFacetRefined: () => false,
};
const helper = {
- addDisjunctiveFacetRefinement: sinon.spy(),
+ addDisjunctiveFacetRefinement: jest.fn(),
};
// When
widget.init({ state, helper, createURL, instantSearchInstance });
// Then
- expect(helper.addDisjunctiveFacetRefinement.called).toBe(false);
+ expect(helper.addDisjunctiveFacetRefinement).not.toHaveBeenCalled();
});
});
diff --git a/src/widgets/toggleRefinement/defaultTemplates.js b/src/widgets/toggleRefinement/defaultTemplates.js
new file mode 100644
index 0000000000..585d437b2a
--- /dev/null
+++ b/src/widgets/toggleRefinement/defaultTemplates.js
@@ -0,0 +1,3 @@
+export default {
+ labelText: '{{name}}',
+};
diff --git a/src/widgets/toggleRefinement/toggleRefinement.js b/src/widgets/toggleRefinement/toggleRefinement.js
new file mode 100644
index 0000000000..2293298f07
--- /dev/null
+++ b/src/widgets/toggleRefinement/toggleRefinement.js
@@ -0,0 +1,145 @@
+import React, { render, unmountComponentAtNode } from 'preact-compat';
+import cx from 'classnames';
+import ToggleRefinement from '../../components/ToggleRefinement/ToggleRefinement.js';
+import connectToggleRefinement from '../../connectors/toggleRefinement/connectToggleRefinement.js';
+import defaultTemplates from './defaultTemplates.js';
+import { getContainerNode, prepareTemplateProps } from '../../lib/utils.js';
+import { component } from '../../lib/suit.js';
+
+const suit = component('ToggleRefinement');
+
+const renderer = ({ containerNode, cssClasses, renderState, templates }) => (
+ { value, createURL, refine, instantSearchInstance },
+ isFirstRendering
+) => {
+ if (isFirstRendering) {
+ renderState.templateProps = prepareTemplateProps({
+ defaultTemplates,
+ templatesConfig: instantSearchInstance.templatesConfig,
+ templates,
+ });
+
+ return;
+ }
+
+ render(
+ refine({ isRefined })}
+ />,
+ containerNode
+ );
+};
+
+const usage = `Usage:
+toggleRefinement({
+ container,
+ attribute,
+ [ on = true ],
+ [ off = undefined ],
+ [ cssClasses.{root, label, labelText, checkbox} ],
+ [ templates.{labelText} ],
+})`;
+
+/**
+ * @typedef {Object} ToggleWidgetCSSClasses
+ * @property {string|string[]} [root] CSS class to add to the root element.
+ * @property {string|string[]} [label] CSS class to add to the label wrapping element
+ * @property {string|string[]} [checkbox] CSS class to add to the checkbox
+ * @property {string|string[]} [labelText] CSS class to add to the label text.
+ */
+
+/**
+ * @typedef {Object} ToggleWidgetTemplates
+ * @property {string|function(object):string} labelText the text that describes the toggle action. This
+ * template receives some contextual information:
+ * - `isRefined` which is `true` if the checkbox is checked
+ * - `count` - the count of the values if the toggle in the next refinements
+ * - `onFacetValue`, `offFacetValue`: objects with `count` (useful to get the other value of `count`)
+ */
+
+/**
+ * @typedef {Object} ToggleWidgetOptions
+ * @property {string|HTMLElement} container Place where to insert the widget in your webpage.
+ * @property {string} attribute Name of the attribute for faceting (eg. "free_shipping").
+ * @property {string|number|boolean} on Value to filter on when checked.
+ * @property {string|number|boolean} off Value to filter on when unchecked.
+ * element (when using the default template). By default when switching to `off`, no refinement will be asked. So you
+ * will get both `true` and `false` results. If you set the off value to `false` then you will get only objects
+ * having `false` has a value for the selected attribute.
+ * @property {ToggleWidgetTemplates} [templates] Templates to use for the widget.
+ * @property {ToggleWidgetCSSClasses} [cssClasses] CSS classes to add.
+ */
+
+/**
+ * The toggleRefinement widget lets the user either:
+ * - switch between two values for a single facetted attribute (free_shipping / not_free_shipping)
+ * - toggleRefinement a faceted value on and off (only 'canon' for brands)
+ *
+ * This widget is particularly useful if you have a boolean value in the records.
+ *
+ * @requirements
+ * The attribute passed to `attribute` must be declared as an
+ * [attribute for faceting](https://www.algolia.com/doc/guides/searching/faceting/#declaring-attributes-for-faceting)
+ * in your Algolia settings.
+ *
+ * @type {WidgetFactory}
+ * @devNovel ToggleRefinement
+ * @category filter
+ * @param {ToggleWidgetOptions} $0 Options for the ToggleRefinement widget.
+ * @return {Widget} A new instance of the ToggleRefinement widget
+ * @example
+ * search.addWidget(
+ * instantsearch.widgets.toggleRefinement({
+ * container: '#free-shipping',
+ * attribute: 'free_shipping',
+ * on: true,
+ * templates: {
+ * labelText: 'Free shipping'
+ * }
+ * })
+ * );
+ */
+export default function toggleRefinement({
+ container,
+ attribute,
+ cssClasses: userCssClasses = {},
+ templates = defaultTemplates,
+ on = true,
+ off,
+} = {}) {
+ if (!container) {
+ throw new Error(usage);
+ }
+
+ const containerNode = getContainerNode(container);
+
+ const cssClasses = {
+ root: cx(suit(), userCssClasses.root),
+ label: cx(suit({ descendantName: 'label' }), userCssClasses.label),
+ checkbox: cx(suit({ descendantName: 'checkbox' }), userCssClasses.checkbox),
+ labelText: cx(
+ suit({ descendantName: 'labelText' }),
+ userCssClasses.labelText
+ ),
+ };
+
+ const specializedRenderer = renderer({
+ containerNode,
+ cssClasses,
+ renderState: {},
+ templates,
+ });
+
+ try {
+ const makeWidget = connectToggleRefinement(specializedRenderer, () =>
+ unmountComponentAtNode(containerNode)
+ );
+ return makeWidget({ attribute, on, off });
+ } catch (error) {
+ throw new Error(usage);
+ }
+}
diff --git a/dev/app/builtin/init-stories.js b/storybook/app/builtin/init-stories.js
similarity index 63%
rename from dev/app/builtin/init-stories.js
rename to storybook/app/builtin/init-stories.js
index d662c6cd31..1ef7cb2fd1 100644
--- a/dev/app/builtin/init-stories.js
+++ b/storybook/app/builtin/init-stories.js
@@ -1,55 +1,55 @@
import initAnalyticsStories from './stories/analytics.stories';
import initBreadcrumbStories from './stories/breadcrumb.stories.js';
-import initClearAllStories from './stories/clear-all.stories';
-import initCurrentRefinedValuesStories from './stories/current-refined-values.stories';
+import initClearRefinementsStories from './stories/clear-refinements.stories';
+import initCurrentRefinementsStories from './stories/current-refinements.stories';
import initGeoSearch from './stories/geo-search.stories';
import initHierarchicalMenu from './stories/hierarchical-menu.stories';
import initHitsStories from './stories/hits.stories';
-import initHitsPerPageSelectorStories from './stories/hits-per-page-selector.stories';
+import initHitsPerPageStories from './stories/hits-per-page.stories';
import initInfiniteHitsStories from './stories/infinite-hits.stories';
import initInstantSearchStories from './stories/instantsearch.stories';
import initMenuStories from './stories/menu.stories';
import initMenuSelectStories from './stories/menu-select.stories';
-import initNumericRefinementListStories from './stories/numeric-refinement-list.stories';
-import initNumericSelectorStories from './stories/numeric-selector.stories';
+import initNumericMenuStories from './stories/numeric-menu.stories';
import initPaginationStories from './stories/pagination.stories';
-import initPriceRangesStories from './stories/price-ranges.stories';
import initRangeInputStories from './stories/range-input.stories.js';
import initRangeSliderStories from './stories/range-slider.stories';
import initRefinementListStories from './stories/refinement-list.stories';
import initReloadStories from './stories/reload.stories';
import initSearchBoxStories from './stories/search-box.stories';
-import initSortBySelectorStories from './stories/sort-by-selector.stories';
-import initStarRatingStories from './stories/star-rating.stories';
+import initSortByStories from './stories/sort-by.stories';
+import initRatingMenuStories from './stories/rating-menu.stories';
import initStatsStories from './stories/stats.stories';
-import initToggleStories from './stories/toggle.stories';
+import initToggleStories from './stories/toggleRefinement.stories';
import initConfigureStories from './stories/configure.stories';
+import initPoweredByStories from './stories/powered-by.stories';
+import initPanelStories from './stories/panel.stories';
export default () => {
initAnalyticsStories();
initBreadcrumbStories();
- initClearAllStories();
- initCurrentRefinedValuesStories();
+ initClearRefinementsStories();
+ initCurrentRefinementsStories();
initGeoSearch();
initHierarchicalMenu();
initHitsStories();
- initHitsPerPageSelectorStories();
+ initHitsPerPageStories();
initInfiniteHitsStories();
initInstantSearchStories();
initMenuStories();
initMenuSelectStories();
- initNumericRefinementListStories();
- initNumericSelectorStories();
+ initNumericMenuStories();
initPaginationStories();
- initPriceRangesStories();
initRangeInputStories();
initRangeSliderStories();
initRefinementListStories();
initReloadStories();
initSearchBoxStories();
- initSortBySelectorStories();
+ initSortByStories();
initStatsStories();
- initStarRatingStories();
+ initRatingMenuStories();
initToggleStories();
initConfigureStories();
+ initPoweredByStories();
+ initPanelStories();
};
diff --git a/dev/app/builtin/stories/analytics.stories.js b/storybook/app/builtin/stories/analytics.stories.js
similarity index 94%
rename from dev/app/builtin/stories/analytics.stories.js
rename to storybook/app/builtin/stories/analytics.stories.js
index 93f84ea173..769a41245e 100644
--- a/dev/app/builtin/stories/analytics.stories.js
+++ b/storybook/app/builtin/stories/analytics.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { action, storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Analytics');
diff --git a/dev/app/builtin/stories/breadcrumb.stories.js b/storybook/app/builtin/stories/breadcrumb.stories.js
similarity index 82%
rename from dev/app/builtin/stories/breadcrumb.stories.js
rename to storybook/app/builtin/stories/breadcrumb.stories.js
index ff62023538..5f95da68c5 100644
--- a/dev/app/builtin/stories/breadcrumb.stories.js
+++ b/storybook/app/builtin/stories/breadcrumb.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Breadcrumb');
@@ -37,6 +37,39 @@ export default () => {
});
})
)
+ .add(
+ 'With custom separators',
+ wrapWithHits(container => {
+ container.innerHTML = `
+
+
+ `;
+
+ window.search.addWidget(
+ instantsearch.widgets.breadcrumb({
+ container: '#breadcrumb',
+ templates: {
+ separator: ' + ',
+ },
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ ],
+ })
+ );
+
+ // Custom Widget to toggle refinement
+ window.search.addWidget({
+ init({ helper }) {
+ helper.toggleRefinement(
+ 'hierarchicalCategories.lvl0',
+ 'Cameras & Camcorders > Digital Cameras'
+ );
+ },
+ });
+ })
+ )
.add(
'with custom home label',
wrapWithHits(container => {
@@ -155,7 +188,7 @@ export default () => {
transformItems: items =>
items.map(item => ({
...item,
- name: `${item.name} (transformed)`,
+ label: `${item.label} (transformed)`,
})),
})
);
diff --git a/storybook/app/builtin/stories/clear-refinements.stories.js b/storybook/app/builtin/stories/clear-refinements.stories.js
new file mode 100644
index 0000000000..08a4ce6c35
--- /dev/null
+++ b/storybook/app/builtin/stories/clear-refinements.stories.js
@@ -0,0 +1,166 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('ClearRefinements');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(
+ container => {
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container,
+ })
+ );
+ },
+ {
+ searchParameters: {
+ disjunctiveFacetsRefinements: { brand: ['Apple'] },
+ disjunctiveFacets: ['brand'],
+ },
+ }
+ )
+ )
+ .add(
+ 'with query only (via includedAttributes)',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container,
+ includedAttributes: ['query'],
+ templates: {
+ resetLabel: 'Clear query',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with query only (via excludedAttributes)',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container,
+ excludedAttributes: [],
+ templates: {
+ resetLabel: 'Clear refinements and query',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with refinements and query',
+ wrapWithHits(
+ container => {
+ const clearRefinementsContainer = document.createElement('div');
+ container.appendChild(clearRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container: clearRefinementsContainer,
+ excludedAttributes: [],
+ templates: {
+ resetLabel: 'Clear refinements and query',
+ },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ },
+ {
+ searchParameters: {
+ disjunctiveFacetsRefinements: { brand: ['Apple'] },
+ disjunctiveFacets: ['brand'],
+ },
+ }
+ )
+ )
+ .add(
+ 'with nothing to clear',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container,
+ })
+ );
+ })
+ )
+ .add(
+ 'with brands excluded (via transformItems)',
+ wrapWithHits(
+ container => {
+ const clearRefinementsContainer = document.createElement('div');
+ container.appendChild(clearRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.clearRefinements({
+ container: clearRefinementsContainer,
+ excludedAttributes: [],
+ transformItems: items =>
+ items.filter(attribute => attribute !== 'brand'),
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ },
+ {
+ searchParameters: {
+ disjunctiveFacetsRefinements: { brand: ['Apple'] },
+ disjunctiveFacets: ['brand'],
+ },
+ }
+ )
+ );
+};
diff --git a/dev/app/builtin/stories/configure.stories.js b/storybook/app/builtin/stories/configure.stories.js
similarity index 93%
rename from dev/app/builtin/stories/configure.stories.js
rename to storybook/app/builtin/stories/configure.stories.js
index f795b18123..da3cc9a90a 100644
--- a/dev/app/builtin/stories/configure.stories.js
+++ b/storybook/app/builtin/stories/configure.stories.js
@@ -2,7 +2,7 @@
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Configure');
diff --git a/storybook/app/builtin/stories/current-refinements.stories.js b/storybook/app/builtin/stories/current-refinements.stories.js
new file mode 100644
index 0000000000..a40e3b479b
--- /dev/null
+++ b/storybook/app/builtin/stories/current-refinements.stories.js
@@ -0,0 +1,437 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits';
+
+const stories = storiesOf('CurrentRefinements');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
+ disjunctiveFacets: ['brand'],
+ numericRefinements: { price: { '>=': [100] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ })
+ )
+ .add(
+ 'with refinementList',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ disjunctiveFacetsRefinements: {
+ brand: ['Google', 'Apple', 'Samsung'],
+ },
+ disjunctiveFacets: ['brand'],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with menu',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const menuContainer = document.createElement('div');
+ container.appendChild(menuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ hierarchicalFacetsRefinements: {
+ brand: ['Samsung'],
+ },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.menu({
+ container: menuContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with hierarchicalMenu',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const hierarchicalMenuContainer = document.createElement('div');
+ container.appendChild(hierarchicalMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ hierarchicalFacetsRefinements: {
+ 'hierarchicalCategories.lvl0': [
+ 'Cameras & Camcorders > Digital Cameras',
+ ],
+ },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.hierarchicalMenu({
+ container: hierarchicalMenuContainer,
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ ],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with toggleRefinement',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const toggleRefinementContainer = document.createElement('div');
+ container.appendChild(toggleRefinementContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.toggleRefinement({
+ container: toggleRefinementContainer,
+ attribute: 'free_shipping',
+ templates: {
+ labelText: 'Free Shipping',
+ },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with numericMenu',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ numericRefinements: { price: { '<=': [10] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with rangeInput',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const rangeInputContainer = document.createElement('div');
+ container.appendChild(rangeInputContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ numericRefinements: { price: { '>=': [100], '<=': [500] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.rangeInput({
+ container: rangeInputContainer,
+ attribute: 'price',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ })
+ );
+ })
+ )
+ .add(
+ 'with only price included',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const toggleRefinementContainer = document.createElement('div');
+ container.appendChild(toggleRefinementContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ numericRefinements: { price: { '<=': [10] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.toggleRefinement({
+ container: toggleRefinementContainer,
+ attribute: 'free_shipping',
+ templates: {
+ labelText: 'Free Shipping',
+ },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ includedAttributes: ['price'],
+ })
+ );
+ })
+ )
+ .add(
+ 'with price and query excluded',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
+ disjunctiveFacets: ['brand'],
+ numericRefinements: { price: { '>=': [100] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ excludedAttributes: ['query', 'price'],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ })
+ )
+ .add(
+ 'with query',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
+ disjunctiveFacets: ['brand'],
+ numericRefinements: { price: { '>=': [100] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ excludedAttributes: [],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ })
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ const currentRefinementsContainer = document.createElement('div');
+ container.appendChild(currentRefinementsContainer);
+ const refinementListContainer = document.createElement('div');
+ container.appendChild(refinementListContainer);
+ const numericMenuContainer = document.createElement('div');
+ container.appendChild(numericMenuContainer);
+
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
+ disjunctiveFacets: ['brand'],
+ numericRefinements: { price: { '>=': [100] } },
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.currentRefinements({
+ container: currentRefinementsContainer,
+ transformItems: items =>
+ items.map(refinementItem => ({
+ ...refinementItem,
+ refinements: refinementItem.refinements.map(item => ({
+ ...item,
+ label: item.label.toUpperCase(),
+ })),
+ })),
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container: refinementListContainer,
+ attribute: 'brand',
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container: numericMenuContainer,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 10, label: '≤ $10' },
+ { start: 10, end: 100, label: '$10–$100' },
+ { start: 100, end: 500, label: '$100–$500' },
+ { start: 500, label: '≥ $500' },
+ ],
+ })
+ );
+ })
+ );
+};
diff --git a/dev/app/builtin/stories/geo-search.stories.js b/storybook/app/builtin/stories/geo-search.stories.js
similarity index 78%
rename from dev/app/builtin/stories/geo-search.stories.js
rename to storybook/app/builtin/stories/geo-search.stories.js
index 0bcc3124b4..9bb19ef1ae 100644
--- a/dev/app/builtin/stories/geo-search.stories.js
+++ b/storybook/app/builtin/stories/geo-search.stories.js
@@ -3,7 +3,7 @@
import { storiesOf, action } from 'dev-novel';
import instantsearchPlacesWidget from 'places.js/instantsearchWidget';
import injectScript from 'scriptjs';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits';
import createInfoBox from '../../utils/create-info-box';
@@ -13,75 +13,41 @@ const wrapWithHitsAndConfiguration = (story, searchParameters) =>
wrapWithHits(story, {
indexName: 'airbnb',
searchParameters: {
- hitsPerPage: 25,
+ hitsPerPage: 20,
...searchParameters,
},
});
const injectGoogleMaps = fn => {
injectScript(
- `https://maps.googleapis.com/maps/api/js?v=3.31&key=${API_KEY}`,
+ `https://maps.googleapis.com/maps/api/js?v=weekly&key=${API_KEY}`,
fn
);
};
export default () => {
const Stories = storiesOf('GeoSearch');
- const radius = 5000;
- const precision = 2500;
const initialZoom = 12;
- const position = {
- lat: 37.7793,
- lng: -122.419,
- };
-
const initialPosition = {
lat: 40.71,
lng: -74.01,
};
- const paddingBoundingBox = {
- top: 41,
- right: 13,
- bottom: 5,
- left: 13,
- };
-
Stories.add(
'default',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
-
window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
})
);
- start();
- })
- )
- ).add(
- 'with transformed items',
- wrapWithHitsAndConfiguration((container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
-
window.search.addWidget(
instantsearch.widgets.geoSearch({
googleReference: window.google,
container,
- transformItems: items =>
- items.map(item => ({
- ...item,
- _geoloc: {
- lat: item._geoloc.lat + 2,
- lng: item._geoloc.lng + 2,
- },
- })),
})
);
@@ -95,7 +61,11 @@ export default () => {
'with IP',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -103,122 +73,34 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
start();
})
)
- )
- .add(
- 'with IP & radius',
- wrapWithHitsAndConfiguration((container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
-
- window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
- initialPosition,
- initialZoom,
- paddingBoundingBox,
- radius,
- })
- );
-
- start();
- })
- )
- )
- .add(
- 'with IP & radius & precision',
- wrapWithHitsAndConfiguration((container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
-
- window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
- initialPosition,
- initialZoom,
- paddingBoundingBox,
- radius,
- precision,
- })
- );
-
- start();
- })
- )
- );
-
- // With position
- Stories.add(
+ ).add(
'with position',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLng: '37.7793, -122.419',
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
googleReference: window.google,
container,
initialZoom,
- paddingBoundingBox,
- position,
})
);
start();
})
)
- )
- .add(
- 'with position & radius',
- wrapWithHitsAndConfiguration((container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
-
- window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
- initialZoom,
- paddingBoundingBox,
- radius,
- position,
- })
- );
-
- start();
- })
- )
- )
- .add(
- 'with position & radius & precision',
- wrapWithHitsAndConfiguration((container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
-
- window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
- initialZoom,
- paddingBoundingBox,
- radius,
- precision,
- position,
- })
- );
-
- start();
- })
- )
- );
+ );
// With Places
Stories.add(
@@ -227,16 +109,21 @@ export default () => {
injectGoogleMaps(() => {
const placesElement = document.createElement('input');
const mapElement = document.createElement('div');
- mapElement.style.height = '500px';
mapElement.style.marginTop = '20px';
container.appendChild(placesElement);
container.appendChild(mapElement);
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundRadius: 20000,
+ })
+ );
+
window.search.addWidget(
instantsearchPlacesWidget({
container: placesElement,
- defaultPosition: [position.lat, position.lng],
+ defaultPosition: ['37.7793', '-122.419'],
})
);
@@ -244,11 +131,8 @@ export default () => {
instantsearch.widgets.geoSearch({
googleReference: window.google,
container: mapElement,
- radius: 20000,
- enableGeolocationWithIP: false,
enableClearMapRefinement: false,
initialZoom,
- paddingBoundingBox,
})
);
@@ -259,10 +143,14 @@ export default () => {
// Only UI
Stories.add(
- 'with control & refine on map move',
+ 'with refine disabled',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -270,9 +158,7 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
- enableRefineControl: true,
- enableRefineOnMapMove: true,
+ enableRefine: false,
})
);
@@ -280,11 +166,40 @@ export default () => {
})
)
)
+ .add(
+ 'with control & refine on map move',
+ wrapWithHitsAndConfiguration((container, start) =>
+ injectGoogleMaps(() => {
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.geoSearch({
+ googleReference: window.google,
+ container,
+ initialPosition,
+ initialZoom,
+ enableRefineControl: true,
+ enableRefineOnMapMove: true,
+ })
+ );
+
+ start();
+ })
+ )
+ )
.add(
'with control & disable refine on map move',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -294,7 +209,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -306,7 +220,11 @@ export default () => {
'without control & refine on map move',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -316,7 +234,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -328,7 +245,11 @@ export default () => {
'without control & disable refine on map move',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -338,7 +259,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -350,20 +270,23 @@ export default () => {
'with custom templates for controls',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
googleReference: window.google,
templates: {
- clear: 're-center ',
+ reset: 're-center ',
toggle: 'Redo search when map moved ',
redo: 'Search this area ',
},
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -375,7 +298,11 @@ export default () => {
'with custom map options',
wrapWithHitsAndConfiguration((container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -386,7 +313,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -400,7 +326,11 @@ export default () => {
injectGoogleMaps(() => {
const logger = action('[GeoSearch] click: builtInMarker');
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -419,7 +349,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -433,7 +362,11 @@ export default () => {
injectGoogleMaps(() => {
const InfoWindow = new window.google.maps.InfoWindow();
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -454,7 +387,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -480,7 +412,11 @@ export default () => {
});
});
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -505,7 +441,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -519,7 +454,11 @@ export default () => {
injectGoogleMaps(() => {
const logger = action('[GeoSearch] click: HTMLMarker');
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -531,21 +470,22 @@ export default () => {
y: 5,
},
}),
- template: `
-
- {{price_formatted}}
-
- `,
events: {
click: ({ event, item, marker, map }) => {
logger(event, item, marker, map);
},
},
},
+ templates: {
+ HTMLMarker: `
+
+ {{price_formatted}}
+
+ `,
+ },
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -561,7 +501,11 @@ export default () => {
pixelOffset: new window.google.maps.Size(0, -30),
});
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -573,11 +517,6 @@ export default () => {
y: 5,
},
}),
- template: `
-
- {{price_formatted}}
-
- `,
events: {
click: ({ item, marker, map }) => {
if (InfoWindow.getMap()) {
@@ -590,10 +529,16 @@ export default () => {
},
},
},
+ templates: {
+ HTMLMarker: `
+
+ {{price_formatted}}
+
+ `,
+ },
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -619,7 +564,11 @@ export default () => {
});
});
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -631,11 +580,6 @@ export default () => {
y: 5,
},
}),
- template: `
-
- {{price_formatted}}
-
- `,
events: {
click: ({ item, marker, map }) => {
if (InfoBoxInstance.getMap()) {
@@ -652,10 +596,16 @@ export default () => {
},
},
},
+ templates: {
+ HTMLMarker: `
+
+ {{price_formatted}}
+
+ `,
+ },
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -703,7 +653,11 @@ export default () => {
removeActiveMarkerClassNames();
});
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -715,11 +669,6 @@ export default () => {
y: 5,
},
}),
- template: `
-
- {{price_formatted}}
-
- `,
events: {
mouseover: ({ item }) => {
removeActiveHitClassNames();
@@ -735,10 +684,16 @@ export default () => {
},
},
},
+ templates: {
+ HTMLMarker: `
+
+ {{price_formatted}}
+
+ `,
+ },
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -747,11 +702,15 @@ export default () => {
)
)
.add(
- 'with URLSync (simulate)',
+ 'with routing (simulate)',
wrapWithHitsAndConfiguration(
(container, start) =>
injectGoogleMaps(() => {
- container.style.height = '600px';
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
window.search.addWidget(
instantsearch.widgets.geoSearch({
@@ -759,7 +718,6 @@ export default () => {
container,
initialPosition,
initialZoom,
- paddingBoundingBox,
})
);
@@ -778,27 +736,34 @@ export default () => {
)
)
.add(
- 'without results',
- wrapWithHitsAndConfiguration(
- (container, start) =>
- injectGoogleMaps(() => {
- container.style.height = '600px';
+ 'with transformed items',
+ wrapWithHitsAndConfiguration((container, start) =>
+ injectGoogleMaps(() => {
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ aroundLatLngViaIP: true,
+ })
+ );
- window.search.addWidget(
- instantsearch.widgets.geoSearch({
- googleReference: window.google,
- container,
- initialPosition,
- initialZoom,
- paddingBoundingBox,
- })
- );
+ window.search.addWidget(
+ instantsearch.widgets.geoSearch({
+ googleReference: window.google,
+ container,
+ builtInMarker: {
+ createOptions: item => ({
+ title: item.name,
+ }),
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: item.name.toUpperCase(),
+ })),
+ })
+ );
- start();
- }),
- {
- query: 'dsdsdsds',
- }
+ start();
+ })
)
);
};
diff --git a/dev/app/builtin/stories/hierarchical-menu.stories.js b/storybook/app/builtin/stories/hierarchical-menu.stories.js
similarity index 63%
rename from dev/app/builtin/stories/hierarchical-menu.stories.js
rename to storybook/app/builtin/stories/hierarchical-menu.stories.js
index c9de784c8e..530c492a1d 100644
--- a/dev/app/builtin/stories/hierarchical-menu.stories.js
+++ b/storybook/app/builtin/stories/hierarchical-menu.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('HierarchicalMenu');
@@ -78,9 +78,62 @@ export default () => {
'hierarchicalCategories.lvl2',
],
rootPath: 'Cameras & Camcorders',
- templates: {
- header: 'Hierarchical categories',
- },
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hierarchicalMenu({
+ container,
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ 'hierarchicalCategories.lvl3',
+ ],
+ limit: 3,
+ showMore: true,
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more and showMoreLimit',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hierarchicalMenu({
+ container,
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ 'hierarchicalCategories.lvl3',
+ ],
+ limit: 3,
+ showMore: true,
+ showMoreLimit: 6,
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more (exhaustive display)',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hierarchicalMenu({
+ container,
+ attributes: [
+ 'hierarchicalCategories.lvl0',
+ 'hierarchicalCategories.lvl1',
+ 'hierarchicalCategories.lvl2',
+ 'hierarchicalCategories.lvl3',
+ ],
+ limit: 200,
+ showMore: true,
+ showMoreLimit: 1000,
})
);
})
diff --git a/dev/app/builtin/stories/hits-per-page-selector.stories.js b/storybook/app/builtin/stories/hits-per-page.stories.js
similarity index 80%
rename from dev/app/builtin/stories/hits-per-page-selector.stories.js
rename to storybook/app/builtin/stories/hits-per-page.stories.js
index 9ab9e53617..c7c7f05cb2 100644
--- a/dev/app/builtin/stories/hits-per-page-selector.stories.js
+++ b/storybook/app/builtin/stories/hits-per-page.stories.js
@@ -1,10 +1,10 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
-import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits';
-const stories = storiesOf('HitsPerPageSelector');
+const stories = storiesOf('HitsPerPage');
export default () => {
stories
@@ -12,7 +12,7 @@ export default () => {
'default',
wrapWithHits(container => {
window.search.addWidget(
- instantsearch.widgets.hitsPerPageSelector({
+ instantsearch.widgets.hitsPerPage({
container,
items: [
{ value: 3, label: '3 per page' },
@@ -27,7 +27,7 @@ export default () => {
'with default hitPerPage to 5',
wrapWithHits(container => {
window.search.addWidget(
- instantsearch.widgets.hitsPerPageSelector({
+ instantsearch.widgets.hitsPerPage({
container,
items: [
{ value: 3, label: '3 per page' },
@@ -42,7 +42,7 @@ export default () => {
'with transformed items',
wrapWithHits(container => {
window.search.addWidget(
- instantsearch.widgets.hitsPerPageSelector({
+ instantsearch.widgets.hitsPerPage({
container,
items: [
{ value: 3, label: '3 per page' },
diff --git a/storybook/app/builtin/stories/hits.stories.js b/storybook/app/builtin/stories/hits.stories.js
new file mode 100644
index 0000000000..6f7091ada3
--- /dev/null
+++ b/storybook/app/builtin/stories/hits.stories.js
@@ -0,0 +1,159 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import algoliasearch from 'algoliasearch/lite';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('Hits');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(instantsearch.widgets.hits({ container }));
+ })
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ name: `${item.name} (transformed)`,
+ })),
+ })
+ );
+ })
+ )
+ .add(
+ 'with highlight function',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ templates: {
+ item(hit) {
+ return instantsearch.highlight({
+ attribute: 'name',
+ hit,
+ });
+ },
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with highlight helper',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ templates: {
+ item:
+ '{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with snippet function',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ attributesToSnippet: ['name', 'description'],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ templates: {
+ item(hit) {
+ return `
+ ${instantsearch.snippet({
+ attribute: 'name',
+ hit,
+ })}
+ ${instantsearch.snippet({
+ attribute: 'description',
+ hit,
+ })}
+ `;
+ },
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with snippet helper',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.configure({
+ attributesToSnippet: ['name', 'description'],
+ })
+ );
+
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ templates: {
+ item: `
+ {{#helpers.snippet}}{ "attribute": "name", "highlightedTagName": "mark" }{{/helpers.snippet}}
+ {{#helpers.snippet}}{ "attribute": "description", "highlightedTagName": "mark" }{{/helpers.snippet}}
`,
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with highlighted array',
+ wrapWithHits(
+ container => {
+ window.search.addWidget(
+ instantsearch.widgets.hits({
+ container,
+ templates: {
+ item: `
+
+
+
+ {{{_highlightResult.name.value}}}
+ \${{price}}
+ {{rating}} stars
+
+
+ {{{_highlightResult.type.value}}}
+
+
+ {{{_highlightResult.description.value}}}
+
+
+ {{#_highlightResult.tags}}
+ {{{value}}}
+ {{/_highlightResult.tags}}
+
+
+
+ `,
+ },
+ })
+ );
+ },
+ {
+ indexName: 'highlight_array',
+ searchClient: algoliasearch(
+ 'KY4PR9ORUL',
+ 'a5ca312adab3b79e14054154efa00b37'
+ ),
+ }
+ )
+ );
+};
diff --git a/dev/app/builtin/stories/infinite-hits.stories.js b/storybook/app/builtin/stories/infinite-hits.stories.js
similarity index 77%
rename from dev/app/builtin/stories/infinite-hits.stories.js
rename to storybook/app/builtin/stories/infinite-hits.stories.js
index ca0c519b47..3c0bc83f4c 100644
--- a/dev/app/builtin/stories/infinite-hits.stories.js
+++ b/storybook/app/builtin/stories/infinite-hits.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('InfiniteHits');
@@ -14,7 +14,6 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.infiniteHits({
container,
- showMoreLabel: 'Show more',
templates: {
item: '{{name}}',
},
@@ -34,9 +33,8 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.infiniteHits({
container,
- showMoreLabel: 'Show more',
cssClasses: {
- showmore: 'button',
+ loadMore: 'button',
},
templates: {
item: '{{name}}',
@@ -45,13 +43,26 @@ export default () => {
);
})
)
+ .add(
+ 'with custom "showMoreText" template',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.infiniteHits({
+ container,
+ templates: {
+ item: '{{name}}',
+ showMoreText: 'Load more',
+ },
+ })
+ );
+ })
+ )
.add(
'with transformed items',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.infiniteHits({
container,
- showMoreLabel: 'Show more',
templates: {
item: '{{name}}',
},
diff --git a/dev/app/builtin/stories/instantsearch.stories.js b/storybook/app/builtin/stories/instantsearch.stories.js
similarity index 100%
rename from dev/app/builtin/stories/instantsearch.stories.js
rename to storybook/app/builtin/stories/instantsearch.stories.js
diff --git a/dev/app/builtin/stories/menu-select.stories.js b/storybook/app/builtin/stories/menu-select.stories.js
similarity index 60%
rename from dev/app/builtin/stories/menu-select.stories.js
rename to storybook/app/builtin/stories/menu-select.stories.js
index 55071fe894..b0f81f2c80 100644
--- a/dev/app/builtin/stories/menu-select.stories.js
+++ b/storybook/app/builtin/stories/menu-select.stories.js
@@ -1,10 +1,10 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
-const stories = storiesOf('Menu-select');
+const stories = storiesOf('MenuSelect');
export default () => {
stories
@@ -14,44 +14,36 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.menuSelect({
container,
- attributeName: 'categories',
+ attribute: 'categories',
})
);
})
)
.add(
- 'with show more and header',
+ 'with custom item template',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.menuSelect({
container,
- attributeName: 'categories',
- limit: 3,
- showMore: {
- templates: {
- active: 'Show less ',
- inactive: 'Show more ',
- },
- limit: 10,
- },
+ attribute: 'categories',
+ limit: 10,
templates: {
- header: 'Categories (menu widget)',
+ item: '{{label}}',
},
})
);
})
)
.add(
- 'with custom item template',
+ 'with custom default option template',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.menuSelect({
container,
- attributeName: 'categories',
+ attribute: 'categories',
limit: 10,
templates: {
- header: 'Categories (menu widget)',
- item: '{{label}}',
+ defaultOption: 'Default choice',
},
})
);
diff --git a/dev/app/builtin/stories/menu.stories.js b/storybook/app/builtin/stories/menu.stories.js
similarity index 54%
rename from dev/app/builtin/stories/menu.stories.js
rename to storybook/app/builtin/stories/menu.stories.js
index 700917fbfa..fc5c5ca722 100644
--- a/dev/app/builtin/stories/menu.stories.js
+++ b/storybook/app/builtin/stories/menu.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Menu');
@@ -14,7 +14,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.menu({
container,
- attributeName: 'categories',
+ attribute: 'categories',
})
);
})
@@ -25,7 +25,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.menu({
container,
- attributeName: 'categories',
+ attribute: 'categories',
transformItems: items =>
items.map(item => ({
...item,
@@ -36,35 +36,51 @@ export default () => {
})
)
.add(
- 'with show more and header',
+ 'with show more',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.menu({
container,
- attributeName: 'categories',
+ attribute: 'categories',
limit: 3,
- showMore: {
- templates: {
- active: 'Show less ',
- inactive: 'Show more ',
- },
- limit: 10,
- },
- templates: {
- header: 'Categories (menu widget)',
- },
+ showMore: true,
})
);
})
)
.add(
- 'as a Select DOM element',
+ 'with show more and showMoreLimit',
wrapWithHits(container => {
window.search.addWidget(
- instantsearch.widgets.menuSelect({
+ instantsearch.widgets.menu({
container,
- attributeName: 'categories',
- limit: 10,
+ attribute: 'categories',
+ limit: 3,
+ showMore: true,
+ showMoreLimit: 6,
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more and templates',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.menu({
+ container,
+ attribute: 'categories',
+ limit: 3,
+ showMore: true,
+ showMoreLimit: 10,
+ templates: {
+ showMoreText: `
+ {{#isShowingMore}}
+ ⬆️
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ ⬇️
+ {{/isShowingMore}}`,
+ },
})
);
})
diff --git a/storybook/app/builtin/stories/numeric-menu.stories.js b/storybook/app/builtin/stories/numeric-menu.stories.js
new file mode 100644
index 0000000000..e1ebbee07e
--- /dev/null
+++ b/storybook/app/builtin/stories/numeric-menu.stories.js
@@ -0,0 +1,62 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits';
+
+const stories = storiesOf('NumericMenu');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 4, label: 'less than 4' },
+ { start: 4, end: 4, label: '4' },
+ { start: 5, end: 10, label: 'between 5 and 10' },
+ { start: 10, label: 'more than 10' },
+ ],
+ cssClasses: {
+ item: 'facet-value',
+ count: 'facet-count pull-right',
+ selectedItem: 'facet-active',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.numericMenu({
+ container,
+ attribute: 'price',
+ items: [
+ { label: 'All' },
+ { end: 4, label: 'less than 4' },
+ { start: 4, end: 4, label: '4' },
+ { start: 5, end: 10, label: 'between 5 and 10' },
+ { start: 10, label: 'more than 10' },
+ ],
+ cssClasses: {
+ item: 'facet-value',
+ count: 'facet-count pull-right',
+ selectedItem: 'facet-active',
+ },
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: `${item.label} (transformed)`,
+ })),
+ })
+ );
+ })
+ );
+};
diff --git a/storybook/app/builtin/stories/pagination.stories.js b/storybook/app/builtin/stories/pagination.stories.js
new file mode 100644
index 0000000000..c9e3ab5ae8
--- /dev/null
+++ b/storybook/app/builtin/stories/pagination.stories.js
@@ -0,0 +1,93 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('Pagination');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ totalPages: 20,
+ })
+ );
+ })
+ )
+ .add(
+ 'with padding',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ padding: 6,
+ })
+ );
+ })
+ )
+ .add(
+ 'without showFirst',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ showFirst: false,
+ })
+ );
+ })
+ )
+ .add(
+ 'without showLast',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ showLast: false,
+ })
+ );
+ })
+ )
+ .add(
+ 'without showPrevious',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ showPrevious: false,
+ })
+ );
+ })
+ )
+ .add(
+ 'without showNext',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ showNext: false,
+ })
+ );
+ })
+ )
+ .add(
+ 'with templates',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.pagination({
+ container,
+ templates: {
+ previous: 'Previous',
+ next: 'Next',
+ first: 'First',
+ last: 'Last',
+ },
+ })
+ );
+ })
+ );
+};
diff --git a/storybook/app/builtin/stories/panel.stories.js b/storybook/app/builtin/stories/panel.stories.js
new file mode 100644
index 0000000000..0467c6f177
--- /dev/null
+++ b/storybook/app/builtin/stories/panel.stories.js
@@ -0,0 +1,65 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('Panel');
+
+export default () => {
+ stories
+ .add(
+ 'with default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.panel({
+ templates: {
+ header: ({ results }) =>
+ `Header ${results ? `| ${results.nbHits} results` : ''}`,
+ footer: 'Footer',
+ },
+ hidden: ({ results }) => results.nbHits === 0,
+ })(instantsearch.widgets.refinementList)({
+ container,
+ attribute: 'brand',
+ })
+ );
+ })
+ )
+ .add(
+ 'with ratingMenu',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.panel({
+ templates: {
+ header: ({ results }) =>
+ `Header ${results ? `| ${results.nbHits} results` : ''}`,
+ footer: 'Footer',
+ },
+ hidden: ({ results }) => results.nbHits === 0,
+ })(instantsearch.widgets.ratingMenu)({
+ container,
+ attribute: 'price',
+ })
+ );
+ })
+ )
+ .add(
+ 'with menu',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.panel({
+ templates: {
+ header: ({ results }) =>
+ `Header ${results ? `| ${results.nbHits} results` : ''}`,
+ footer: 'Footer',
+ },
+ hidden: ({ results }) => results.nbHits === 0,
+ })(instantsearch.widgets.menu)({
+ container,
+ attribute: 'brand',
+ })
+ );
+ })
+ );
+};
diff --git a/storybook/app/builtin/stories/powered-by.stories.js b/storybook/app/builtin/stories/powered-by.stories.js
new file mode 100644
index 0000000000..fcbc221614
--- /dev/null
+++ b/storybook/app/builtin/stories/powered-by.stories.js
@@ -0,0 +1,30 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('PoweredBy');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(instantsearch.widgets.poweredBy({ container }));
+ })
+ )
+ .add(
+ 'with dark theme',
+ wrapWithHits(container => {
+ container.style.backgroundColor = '#282c34';
+
+ window.search.addWidget(
+ instantsearch.widgets.poweredBy({
+ container,
+ theme: 'dark',
+ })
+ );
+ })
+ );
+};
diff --git a/dev/app/builtin/stories/range-input.stories.js b/storybook/app/builtin/stories/range-input.stories.js
similarity index 68%
rename from dev/app/builtin/stories/range-input.stories.js
rename to storybook/app/builtin/stories/range-input.stories.js
index 24b560621b..59e6dce597 100644
--- a/dev/app/builtin/stories/range-input.stories.js
+++ b/storybook/app/builtin/stories/range-input.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('RangeInput');
@@ -14,10 +14,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
- templates: {
- header: 'Range input',
- },
+ attribute: 'price',
})
);
})
@@ -28,87 +25,74 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
+ attribute: 'price',
min: 500,
max: 0,
- templates: {
- header: 'Range input',
- },
})
);
})
)
.add(
- 'collapsible',
+ 'with floating number',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
- collapsible: true,
- templates: {
- header: 'Range input',
- },
+ attribute: 'price',
+ precision: 2,
})
);
})
)
.add(
- 'with floating number',
+ 'with min boundary',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
- precision: 2,
- templates: {
- header: 'Range input',
- },
+ attribute: 'price',
+ min: 10,
})
);
})
)
.add(
- 'with min boundary',
+ 'with max boundary',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
- min: 10,
- templates: {
- header: 'Range input',
- },
+ attribute: 'price',
+ max: 500,
})
);
})
)
.add(
- 'with max boundary',
+ 'with min & max boundaries',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
+ attribute: 'price',
+ min: 10,
max: 500,
- templates: {
- header: 'Range input',
- },
})
);
})
)
.add(
- 'with min & max boundaries',
+ 'with templates',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeInput({
container,
- attributeName: 'price',
+ attribute: 'price',
min: 10,
max: 500,
templates: {
- header: 'Range input',
+ separatorText: '→',
+ submitText: 'Refine',
},
})
);
diff --git a/dev/app/builtin/stories/range-slider.stories.js b/storybook/app/builtin/stories/range-slider.stories.js
similarity index 83%
rename from dev/app/builtin/stories/range-slider.stories.js
rename to storybook/app/builtin/stories/range-slider.stories.js
index 5c3be70bc9..9cd2a05d64 100644
--- a/dev/app/builtin/stories/range-slider.stories.js
+++ b/storybook/app/builtin/stories/range-slider.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('RangeSlider');
@@ -14,7 +14,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
@@ -33,7 +33,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
@@ -48,30 +48,13 @@ export default () => {
);
})
)
- .add(
- 'collapsible',
- wrapWithHits(container => {
- window.search.addWidget(
- instantsearch.widgets.rangeSlider({
- container,
- attributeName: 'price',
- collapsible: {
- collapsed: false,
- },
- templates: {
- header: 'Price',
- },
- })
- );
- })
- )
.add(
'with step',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
step: 500,
tooltips: {
format(rawValue) {
@@ -88,7 +71,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
pips: false,
tooltips: {
format(rawValue) {
@@ -105,7 +88,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
@@ -125,7 +108,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
@@ -145,7 +128,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
@@ -165,7 +148,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.rangeSlider({
container,
- attributeName: 'price',
+ attribute: 'price',
templates: {
header: 'Price',
},
diff --git a/storybook/app/builtin/stories/rating-menu.stories.js b/storybook/app/builtin/stories/rating-menu.stories.js
new file mode 100644
index 0000000000..dd94d0f394
--- /dev/null
+++ b/storybook/app/builtin/stories/rating-menu.stories.js
@@ -0,0 +1,35 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits';
+
+const stories = storiesOf('RatingMenu');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.ratingMenu({
+ container,
+ attribute: 'rating',
+ max: 5,
+ })
+ );
+ })
+ )
+ .add(
+ 'with disabled item',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.ratingMenu({
+ container,
+ attribute: 'rating',
+ max: 7,
+ })
+ );
+ })
+ );
+};
diff --git a/dev/app/builtin/stories/refinement-list.stories.js b/storybook/app/builtin/stories/refinement-list.stories.js
similarity index 53%
rename from dev/app/builtin/stories/refinement-list.stories.js
rename to storybook/app/builtin/stories/refinement-list.stories.js
index 979a6acc6c..83d4aa1fd1 100644
--- a/dev/app/builtin/stories/refinement-list.stories.js
+++ b/storybook/app/builtin/stories/refinement-list.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('RefinementList');
@@ -14,12 +14,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
- limit: 10,
- templates: {
- header: 'Brands',
- },
+ attribute: 'brand',
})
);
})
@@ -30,18 +25,45 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
+ attribute: 'brand',
+ limit: 3,
+ showMore: true,
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more and showMoreLimit',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container,
+ attribute: 'brand',
limit: 3,
+ showMore: true,
+ showMoreLimit: 6,
+ })
+ );
+ })
+ )
+ .add(
+ 'with show more and templates',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.refinementList({
+ container,
+ attribute: 'brand',
+ limit: 3,
+ showMore: true,
+ showMoreLimit: 10,
templates: {
- header: 'Brands with show more',
- },
- showMore: {
- templates: {
- active: 'Show less ',
- inactive: 'Show more ',
- },
- limit: 10,
+ showMoreText: `
+ {{#isShowingMore}}
+ ⬆️
+ {{/isShowingMore}}
+ {{^isShowingMore}}
+ ⬇️
+ {{/isShowingMore}}`,
},
})
);
@@ -53,36 +75,23 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
- limit: 10,
- templates: {
- header: 'Searchable brands',
- },
- searchForFacetValues: {
- placeholder: 'Find other brands...',
- templates: {
- noResults: 'No results',
- },
- },
+ attribute: 'brand',
+ searchable: true,
})
);
})
)
.add(
- 'with search inside items (using the default noResults template)',
+ 'with search inside items (using a custom searchableNoResults template)',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
- limit: 10,
+ attribute: 'brand',
+ searchable: true,
+ searchablePlaceholder: 'Find other brands...',
templates: {
- header: 'Searchable brands',
- },
- searchForFacetValues: {
- placeholder: 'Find other brands...',
+ searchableNoResults: 'No results found',
},
})
);
@@ -94,23 +103,12 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'price_range',
+ attribute: 'price_range',
operator: 'and',
- limit: 10,
cssClasses: {
- header: 'facet-title',
item: 'facet-value checkbox',
count: 'facet-count pull-right',
- active: 'facet-active',
- },
- templates: {
- header: 'Price ranges',
- },
- transformData(data) {
- data.label = data.label
- .replace(/(\d+) - (\d+)/, '$$$1 - $$$2')
- .replace(/> (\d+)/, '> $$$1');
- return data;
+ selectedItem: 'facet-active',
},
})
);
@@ -122,12 +120,7 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
- limit: 10,
- templates: {
- header: 'Transformed brands',
- },
+ attribute: 'brand',
transformItems: items =>
items.map(item => ({
...item,
@@ -144,18 +137,8 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.refinementList({
container,
- attributeName: 'brand',
- operator: 'or',
- limit: 10,
- templates: {
- header: 'Transformed searchable brands',
- },
- searchForFacetValues: {
- placeholder: 'Find other brands...',
- templates: {
- noResults: 'No results',
- },
- },
+ attribute: 'brand',
+ searchable: true,
transformItems: items =>
items.map(item => ({
...item,
diff --git a/dev/app/builtin/stories/reload.stories.js b/storybook/app/builtin/stories/reload.stories.js
similarity index 93%
rename from dev/app/builtin/stories/reload.stories.js
rename to storybook/app/builtin/stories/reload.stories.js
index d4e3009c0a..6799e729a0 100644
--- a/dev/app/builtin/stories/reload.stories.js
+++ b/storybook/app/builtin/stories/reload.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Refresh');
diff --git a/dev/app/builtin/stories/search-box.stories.js b/storybook/app/builtin/stories/search-box.stories.js
similarity index 50%
rename from dev/app/builtin/stories/search-box.stories.js
rename to storybook/app/builtin/stories/search-box.stories.js
index 038f8549fa..a62525530e 100644
--- a/dev/app/builtin/stories/search-box.stories.js
+++ b/storybook/app/builtin/stories/search-box.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('SearchBox');
@@ -14,110 +14,77 @@ export default () => {
window.search.addWidget(
instantsearch.widgets.searchBox({
container,
- placeholder: 'Search for products',
- poweredBy: true,
})
);
})
)
.add(
- 'display loading indicator',
+ 'with a custom placeholder',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.searchBox({
container,
placeholder: 'Search for products',
- poweredBy: true,
- loadingIndicator: true,
})
);
})
)
.add(
- 'display loading indicator with a template',
+ 'with autofocus',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.searchBox({
container,
- placeholder: 'Search for products',
- poweredBy: true,
- loadingIndicator: {
- template: '⚡️',
- },
+ autofocus: true,
})
);
})
)
.add(
- 'with custom templates',
+ 'do not display the loading indicator',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.searchBox({
container,
- placeholder: 'Search for products',
- poweredBy: true,
- magnifier: {
- template: '🔍
',
- },
- reset: {
- template: '✖️
',
- },
- templates: {
- poweredBy: 'Algolia',
- },
+ showLoadingIndicator: false,
})
);
})
)
.add(
- 'search on enter',
+ 'display loading indicator with a template',
wrapWithHits(container => {
window.search.addWidget(
instantsearch.widgets.searchBox({
container,
- placeholder: 'Search for products',
- poweredBy: true,
- searchOnEnterKeyPressOnly: true,
- })
- );
- })
- )
- .add(
- 'input with initial value',
- wrapWithHits(container => {
- container.innerHTML = ' ';
- const input = container.firstChild;
- container.appendChild(input);
- window.search.addWidget(
- instantsearch.widgets.searchBox({
- container: input,
+ templates: {
+ loadingIndicator: '⚡️',
+ },
})
);
})
)
.add(
- 'with a provided input',
+ 'with custom templates',
wrapWithHits(container => {
- container.innerHTML = ' ';
- const input = container.firstChild;
- container.appendChild(input);
window.search.addWidget(
instantsearch.widgets.searchBox({
- container: input,
+ container,
+ templates: {
+ submit: '🔍
',
+ reset: '✖️
',
+ },
})
);
})
)
.add(
- 'with a provided input and the loading indicator',
+ 'search on enter',
wrapWithHits(container => {
- container.innerHTML = ' ';
- const input = container.firstChild;
- container.appendChild(input);
window.search.addWidget(
instantsearch.widgets.searchBox({
- container: input,
- loadingIndicator: true,
+ container,
+ searchAsYouType: false,
})
);
})
diff --git a/storybook/app/builtin/stories/sort-by.stories.js b/storybook/app/builtin/stories/sort-by.stories.js
new file mode 100644
index 0000000000..cfe6a86e7e
--- /dev/null
+++ b/storybook/app/builtin/stories/sort-by.stories.js
@@ -0,0 +1,46 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits';
+
+const stories = storiesOf('SortBy');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.sortBy({
+ container,
+ items: [
+ { value: 'instant_search', label: 'Most relevant' },
+ { value: 'instant_search_price_asc', label: 'Lowest price' },
+ { value: 'instant_search_price_desc', label: 'Highest price' },
+ ],
+ })
+ );
+ })
+ )
+ .add(
+ 'with transformed items',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.sortBy({
+ container,
+ items: [
+ { value: 'instant_search', label: 'Most relevant' },
+ { value: 'instant_search_price_asc', label: 'Lowest price' },
+ { value: 'instant_search_price_desc', label: 'Highest price' },
+ ],
+ transformItems: items =>
+ items.map(item => ({
+ ...item,
+ label: item.label.toUpperCase(),
+ })),
+ })
+ );
+ })
+ );
+};
diff --git a/dev/app/builtin/stories/stats.stories.js b/storybook/app/builtin/stories/stats.stories.js
similarity index 87%
rename from dev/app/builtin/stories/stats.stories.js
rename to storybook/app/builtin/stories/stats.stories.js
index f3224e99bb..2a7c35c5e0 100644
--- a/dev/app/builtin/stories/stats.stories.js
+++ b/storybook/app/builtin/stories/stats.stories.js
@@ -1,7 +1,7 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../../../index';
+import instantsearch from '../../instantsearch';
import { wrapWithHits } from '../../utils/wrap-with-hits.js';
const stories = storiesOf('Stats');
diff --git a/storybook/app/builtin/stories/toggleRefinement.stories.js b/storybook/app/builtin/stories/toggleRefinement.stories.js
new file mode 100644
index 0000000000..a987a928f5
--- /dev/null
+++ b/storybook/app/builtin/stories/toggleRefinement.stories.js
@@ -0,0 +1,52 @@
+/* eslint-disable import/default */
+
+import { storiesOf } from 'dev-novel';
+import instantsearch from '../../instantsearch';
+import { wrapWithHits } from '../../utils/wrap-with-hits.js';
+
+const stories = storiesOf('ToggleRefinement');
+
+export default () => {
+ stories
+ .add(
+ 'default',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.toggleRefinement({
+ container,
+ attribute: 'free_shipping',
+ })
+ );
+ })
+ )
+ .add(
+ 'with label',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.toggleRefinement({
+ container,
+ attribute: 'free_shipping',
+ templates: {
+ labelText: 'Free Shipping (toggle single value)',
+ },
+ })
+ );
+ })
+ )
+ .add(
+ 'with on & off values',
+ wrapWithHits(container => {
+ window.search.addWidget(
+ instantsearch.widgets.toggleRefinement({
+ container,
+ attribute: 'brand',
+ on: 'Sony',
+ off: 'Canon',
+ templates: {
+ labelText: 'Canon (not checked) or sony (checked)',
+ },
+ })
+ );
+ })
+ );
+};
diff --git a/dev/app/index.js b/storybook/app/index.js
similarity index 72%
rename from dev/app/index.js
rename to storybook/app/index.js
index 1af8f2dcee..f515a24eb1 100644
--- a/dev/app/index.js
+++ b/storybook/app/index.js
@@ -1,11 +1,7 @@
import { registerDisposer, start } from 'dev-novel';
import initBuiltInWidgets from './builtin/init-stories';
-import initJqueryWidgets from './jquery/init-stories';
import initUnmountWidgets from './init-unmount-widgets.js';
-
import '../style.css';
-import '../../src/css/instantsearch.scss';
-import '../../src/css/instantsearch-theme-algolia.scss';
registerDisposer(() => {
window.search = undefined;
@@ -16,10 +12,6 @@ const q = window.location.search;
let selectedTab = '';
switch (true) {
- case q.includes('widgets=jquery'):
- initJqueryWidgets();
- selectedTab = 'jquery';
- break;
case q.includes('widgets=unmount'):
initUnmountWidgets();
selectedTab = 'unmount';
@@ -32,9 +24,6 @@ const selectStories = document.createElement('div');
selectStories.className = 'story-selector';
selectStories.innerHTML = `
Built-in
- Connectors with jQuery
Disposable widgets
diff --git a/dev/app/init-unmount-widgets.js b/storybook/app/init-unmount-widgets.js
similarity index 78%
rename from dev/app/init-unmount-widgets.js
rename to storybook/app/init-unmount-widgets.js
index 4290bca27d..da2a04eecd 100644
--- a/dev/app/init-unmount-widgets.js
+++ b/storybook/app/init-unmount-widgets.js
@@ -1,7 +1,6 @@
/* eslint-disable import/default */
import { storiesOf } from 'dev-novel';
-import instantsearch from '../../index.js';
-
+import instantsearch from './instantsearch';
import { wrapWithHits } from './utils/wrap-with-hits.js';
function wrapWithUnmount(getWidget, params) {
@@ -61,10 +60,10 @@ function wrapWithUnmount(getWidget, params) {
}
export default () => {
- storiesOf('ClearAll').add(
+ storiesOf('ClearRefinements').add(
'default',
wrapWithUnmount(
- container => instantsearch.widgets.clearAll({ container }),
+ container => instantsearch.widgets.clearRefinements({ container }),
{
searchParameters: {
disjunctiveFacetsRefinements: { brand: ['Apple'] },
@@ -74,10 +73,10 @@ export default () => {
)
);
- storiesOf('CurrentRefinedValues').add(
+ storiesOf('CurrentRefinements').add(
'default',
wrapWithUnmount(
- container => instantsearch.widgets.currentRefinedValues({ container }),
+ container => instantsearch.widgets.currentRefinements({ container }),
{
searchParameters: {
disjunctiveFacetsRefinements: { brand: ['Apple', 'Samsung'] },
@@ -145,7 +144,7 @@ export default () => {
storiesOf('HitsPerPage').add(
'default',
wrapWithUnmount(container =>
- instantsearch.widgets.hitsPerPageSelector({
+ instantsearch.widgets.hitsPerPage({
container,
items: [
{ value: 3, label: '3 per page' },
@@ -161,7 +160,7 @@ export default () => {
wrapWithUnmount(container =>
instantsearch.widgets.infiniteHits({
container,
- showMoreLabel: 'Show more',
+ loadMoreLabel: 'Show more',
templates: {
item: '{{name}}',
},
@@ -179,12 +178,12 @@ export default () => {
)
);
- storiesOf('NumericRefinementList').add(
+ storiesOf('NumericMenu').add(
'default',
wrapWithUnmount(container =>
- instantsearch.widgets.numericRefinementList({
+ instantsearch.widgets.numericMenu({
container,
- attributeName: 'price',
+ attribute: 'price',
operator: 'or',
options: [
{ name: 'All' },
@@ -206,42 +205,12 @@ export default () => {
)
);
- storiesOf('NumericSelector').add(
- 'default',
- wrapWithUnmount(container =>
- instantsearch.widgets.numericSelector({
- container,
- operator: '>=',
- attributeName: 'popularity',
- options: [
- { label: 'Default', value: 0 },
- { label: 'Top 10', value: 9991 },
- { label: 'Top 100', value: 9901 },
- { label: 'Top 500', value: 9501 },
- ],
- })
- )
- );
-
storiesOf('Pagination').add(
'default',
wrapWithUnmount(container =>
instantsearch.widgets.pagination({
container,
- maxPages: 20,
- })
- )
- );
-
- storiesOf('PriceRanges').add(
- 'default',
- wrapWithUnmount(container =>
- instantsearch.widgets.priceRanges({
- container,
- attributeName: 'price',
- templates: {
- header: 'Price ranges',
- },
+ totalPages: 20,
})
)
);
@@ -272,33 +241,27 @@ export default () => {
)
);
- storiesOf('SortBySelector').add(
+ storiesOf('SortBy').add(
'default',
wrapWithUnmount(container =>
- instantsearch.widgets.sortBySelector({
+ instantsearch.widgets.sortBy({
container,
- indices: [
- { name: 'instant_search', label: 'Most relevant' },
- { name: 'instant_search_price_asc', label: 'Lowest price' },
- { name: 'instant_search_price_desc', label: 'Highest price' },
+ items: [
+ { value: 'instant_search', label: 'Most relevant' },
+ { value: 'instant_search_price_asc', label: 'Lowest price' },
+ { value: 'instant_search_price_desc', label: 'Highest price' },
],
})
)
);
- storiesOf('StarRating').add(
+ storiesOf('RatingMenu').add(
'default',
wrapWithUnmount(container =>
- instantsearch.widgets.starRating({
+ instantsearch.widgets.ratingMenu({
container,
- attributeName: 'rating',
+ attribute: 'rating',
max: 5,
- labels: {
- andUp: '& Up',
- },
- templates: {
- header: 'Rating',
- },
})
)
);
@@ -308,7 +271,7 @@ export default () => {
wrapWithUnmount(container => instantsearch.widgets.stats({ container }))
);
- storiesOf('Toggle').add(
+ storiesOf('ToggleRefinement').add(
'default',
wrapWithUnmount(container =>
instantsearch.widgets.toggle({
diff --git a/storybook/app/instantsearch.js b/storybook/app/instantsearch.js
new file mode 100644
index 0000000000..e398eb40ab
--- /dev/null
+++ b/storybook/app/instantsearch.js
@@ -0,0 +1,3 @@
+import instantsearch from '../../src/index';
+
+export default instantsearch;
diff --git a/dev/app/utils/all-items.html b/storybook/app/utils/all-items.html
similarity index 100%
rename from dev/app/utils/all-items.html
rename to storybook/app/utils/all-items.html
diff --git a/dev/app/utils/create-info-box.js b/storybook/app/utils/create-info-box.js
similarity index 100%
rename from dev/app/utils/create-info-box.js
rename to storybook/app/utils/create-info-box.js
diff --git a/dev/app/utils/item.html b/storybook/app/utils/item.html
similarity index 100%
rename from dev/app/utils/item.html
rename to storybook/app/utils/item.html
diff --git a/dev/app/utils/no-results.html b/storybook/app/utils/no-results.html
similarity index 100%
rename from dev/app/utils/no-results.html
rename to storybook/app/utils/no-results.html
diff --git a/dev/app/utils/wrap-with-hits.js b/storybook/app/utils/wrap-with-hits.js
similarity index 87%
rename from dev/app/utils/wrap-with-hits.js
rename to storybook/app/utils/wrap-with-hits.js
index cb3d9acb64..3ca8172073 100644
--- a/dev/app/utils/wrap-with-hits.js
+++ b/storybook/app/utils/wrap-with-hits.js
@@ -1,7 +1,8 @@
/* eslint-disable import/default */
import { action } from 'dev-novel';
-import instantsearch from '../../../index.js';
+import algoliasearch from 'algoliasearch/lite';
+import instantsearch from '../instantsearch';
import item from './item.html';
import empty from './no-results.html';
@@ -10,18 +11,16 @@ export const wrapWithHits = (
instantSearchConfig = {}
) => container => {
const {
- appId = 'latency',
- apiKey = '6be0576ff61c053d5f9a3225e2a90f76',
indexName = 'instant_search',
+ searchClient = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'),
searchParameters = {},
...otherInstantSearchConfig
} = instantSearchConfig;
const urlLogger = action('Routing state');
window.search = instantsearch({
- appId,
- apiKey,
indexName,
+ searchClient,
searchParameters: {
hitsPerPage: 3,
...searchParameters,
@@ -70,7 +69,7 @@ export const wrapWithHits = (
window.search.addWidget(
instantsearch.widgets.pagination({
container: '#results-pagination-container',
- maxPages: 20,
+ totalPages: 20,
})
);
@@ -84,6 +83,3 @@ export const wrapWithHits = (
window.search.start();
});
};
-
-export const wrapWithHitsAndJquery = fn =>
- wrapWithHits(container => fn(window.$(container)));
diff --git a/dev/style.css b/storybook/style.css
similarity index 94%
rename from dev/style.css
rename to storybook/style.css
index 2cc0f4baa9..1cd6f7b273 100644
--- a/dev/style.css
+++ b/storybook/style.css
@@ -52,6 +52,18 @@
/* GeoSearch */
+.ais-GeoSearch {
+ width: 850px;
+ height: 500px;
+ margin: 0 auto;
+}
+
+.algolia-places {
+ display: block !important;
+ width: 850px;
+ margin: 0 auto;
+}
+
.my-custom-marker {
position: relative;
background-color: white;
diff --git a/storybook/template.html b/storybook/template.html
new file mode 100644
index 0000000000..72090c496d
--- /dev/null
+++ b/storybook/template.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Instant search demo built with instantsearch.js
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/webpack.config.js b/storybook/webpack.config.js
similarity index 97%
rename from dev/webpack.config.js
rename to storybook/webpack.config.js
index 7fd76b36e5..ff2b9ffb12 100644
--- a/dev/webpack.config.js
+++ b/storybook/webpack.config.js
@@ -2,7 +2,6 @@
/* eslint camelcase: 0 */
const path = require('path');
-
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPluging = require('html-webpack-plugin');
@@ -20,12 +19,12 @@ module.exports = {
bundle: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
- './dev/app',
+ './storybook/app',
],
- instantsearch: './index.js',
+ instantsearch: './src/index.js',
}
: {
- bundle: './dev/app',
+ bundle: './storybook/app',
},
output: {
diff --git a/yarn.lock b/yarn.lock
index d6ce001f31..16323f406e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,6 +9,13 @@
dependencies:
"@babel/highlight" "7.0.0-beta.44"
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
"@babel/code-frame@^7.0.0-beta.35":
version "7.0.0-beta.40"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz#37e2b0cf7c56026b4b21d3927cadf81adec32ac6"
@@ -68,6 +75,15 @@
esutils "^2.0.2"
js-tokens "^3.0.0"
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
"@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "http://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
@@ -128,6 +144,11 @@
traverse "^0.6.6"
unified "^6.1.6"
+"@types/estree@0.0.39":
+ version "0.0.39"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
+ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+
"@types/node@*":
version "9.4.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e"
@@ -290,7 +311,7 @@ algoliasearch-helper@^2.26.0:
qs "^6.5.1"
util "^0.10.3"
-algoliasearch@^3.27.0, algoliasearch@^3.27.1:
+algoliasearch@3.30.0, algoliasearch@^3.27.1:
version "3.30.0"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.30.0.tgz#355585e49b672e5f71d45b9c2b371ecdff129cd1"
integrity sha512-FuinyPgNn0MeAHm9pan6rLgY6driY3mcTo4AWNBMY1MUReeA5PQA8apV/3SNXqA5bbsuvMvmA0ZrVzrOmEeQTA==
@@ -1057,6 +1078,20 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
+babel-plugin-external-helpers@6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1"
+ integrity sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-inline-replace-variables@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-inline-replace-variables/-/babel-plugin-inline-replace-variables-1.3.1.tgz#9fbb8dd43229c777695e14ea0d3d781f048fdc0f"
+ integrity sha1-n7uN1DIpx3dpXhTqDT14HwSP3A8=
+ dependencies:
+ babylon "^6.17.0"
+
babel-plugin-istanbul@^4.1.6:
version "4.1.6"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
@@ -1738,7 +1773,7 @@ babylon@7.0.0-beta.44:
resolved "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
integrity sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==
-babylon@^6.18.0:
+babylon@^6.17.0, babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
@@ -1909,6 +1944,19 @@ boxen@^1.2.1:
term-size "^1.2.0"
widest-line "^2.0.0"
+boxen@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/boxen/-/boxen-2.0.0.tgz#46ba3953b1a3d99aaf89ad8c7104a32874934a58"
+ integrity sha512-9DK9PQqcOpsvlKOK3f3lVK+vQsqH4JDGMX73FCWcHRxQQtop1U8urn4owrt5rnc2NgZAJ6wWjTDBc7Fhv+vz/w==
+ dependencies:
+ ansi-align "^2.0.0"
+ camelcase "^5.0.0"
+ chalk "^2.4.1"
+ cli-boxes "^1.0.0"
+ string-width "^2.1.1"
+ term-size "^1.2.0"
+ widest-line "^2.0.0"
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1957,6 +2005,14 @@ brotli-size@0.0.1:
duplexer "^0.1.1"
iltorb "^1.0.9"
+brotli-size@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/brotli-size/-/brotli-size-0.0.3.tgz#1d3855b38f182591a6f69da1516131676e5f62f2"
+ integrity sha512-bBIdd8uUGxKGldAVykxOqPegl+HlIm4FpXJamwWw5x77WCE8jO7AhXFE1YXOhOB28gS+2pTQete0FqRE6U5hQQ==
+ dependencies:
+ duplexer "^0.1.1"
+ iltorb "^2.0.5"
+
browser-process-hrtime@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
@@ -2115,6 +2171,11 @@ builtin-modules@^1.0.0:
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
+ integrity sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg==
+
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -2238,6 +2299,11 @@ camelcase@^4.0.0, camelcase@^4.1.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+camelcase@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+ integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
+
caniuse-api@^1.5.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
@@ -2660,7 +2726,7 @@ colors@1.0.3:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
-colors@1.3.2:
+colors@1.3.2, colors@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b"
integrity sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==
@@ -2709,6 +2775,11 @@ commander@~2.12.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
integrity sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+ integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -3296,14 +3367,6 @@ css-to-react-native@^2.0.3:
fbjs "^0.8.5"
postcss-value-parser "^3.3.0"
-css-tree@1.0.0-alpha.27:
- version "1.0.0-alpha.27"
- resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.27.tgz#f211526909c7dc940843d83b9376ed98ddb8de47"
- integrity sha512-BAYp9FyN4jLXjfvRpTDchBllDptqlK9I7OsagXCG9Am5C+5jc8eRZHgqb9x500W2OKS14MMlpQc/nmh/aA7TEQ==
- dependencies:
- mdn-data "^1.0.0"
- source-map "^0.5.3"
-
css-value@~0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea"
@@ -3367,23 +3430,6 @@ cssnano@^3.10.0:
postcss-value-parser "^3.2.3"
postcss-zindex "^2.0.1"
-csso-cli@1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/csso-cli/-/csso-cli-1.1.0.tgz#c787557612fae522875788bfb6e69594fe79b3c7"
- integrity sha1-x4dVdhL65SKHV4i/tuaVlP55s8c=
- dependencies:
- chokidar "^1.6.1"
- clap "^1.0.9"
- csso "^3.2.0"
- source-map "^0.5.3"
-
-csso@^3.2.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.0.tgz#acdbba5719e2c87bc801eadc032764b2e4b9d4e7"
- integrity sha512-WtJjFP3ZsSdWhiZr4/k1B9uHPgYjFYnDxfbaJxk1hz5PDLIJ5BCRWkJqaztZ0DbP8d2ZIVwUPIJb2YmCwkPaMw==
- dependencies:
- css-tree "1.0.0-alpha.27"
-
csso@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
@@ -3486,11 +3532,23 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
+deep-assign@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-2.0.0.tgz#ebe06b1f07f08dae597620e3dd1622f371a1c572"
+ integrity sha1-6+BrHwfwja5ZdiDj3RYi83GhxXI=
+ dependencies:
+ is-obj "^1.0.0"
+
deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@@ -4363,6 +4421,16 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
+ integrity sha1-va/oCVOD2EFNXcLs9MkXO225QS4=
+
+estree-walker@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
+ integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==
+
esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -4757,6 +4825,11 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
+filesize@^3.6.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+ integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+
fill-range@^2.1.0:
version "2.2.3"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
@@ -5391,6 +5464,14 @@ gzip-size@^4.0.0:
duplexer "^0.1.1"
pify "^3.0.0"
+gzip-size@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80"
+ integrity sha512-5iI7omclyqrnWw4XbXAmGhPsABkSIDQonv2K0h61lybgofWa6iZyvrI3r2zsJH4P8Nb64fFVzlvfhs0g7BBxAA==
+ dependencies:
+ duplexer "^0.1.1"
+ pify "^3.0.0"
+
handle-thing@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -5829,6 +5910,16 @@ iltorb@^1.0.9:
node-gyp "^3.6.2"
prebuild-install "^2.3.0"
+iltorb@^2.0.5:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/iltorb/-/iltorb-2.4.1.tgz#3ae14f0a76ba880503884a2fe630b1f748eb4c17"
+ integrity sha512-huyAN7dSNe2b7VAl5AyvaeZ8XTcDTSF1b8JVYDggl+SBfHsORq3qMZeesZW7zoEy21s15SiERAITWT5cwxu1Uw==
+ dependencies:
+ detect-libc "^1.0.3"
+ npmlog "^4.1.2"
+ prebuild-install "^5.2.1"
+ which-pm-runs "^1.0.0"
+
image-size@^0.5.0:
version "0.5.5"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
@@ -6301,6 +6392,11 @@ is-installed-globally@^0.1.0:
global-dirs "^0.1.0"
is-path-inside "^1.0.0"
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+ integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
+
is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
@@ -7016,6 +7112,11 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.1:
version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
@@ -7531,6 +7632,13 @@ macaddress@^0.2.8:
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
integrity sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=
+magic-string@^0.25.1:
+ version "0.25.1"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e"
+ integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==
+ dependencies:
+ sourcemap-codec "^1.4.1"
+
make-dir@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
@@ -7610,11 +7718,6 @@ md5@^2.2.1:
crypt "~0.0.1"
is-buffer "~1.1.1"
-mdn-data@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.0.tgz#a7056319da95a2d0881267d7263075042eb061e2"
- integrity sha512-jC6B3BFC07cCOU8xx1d+sQtDkVIpGKWv4TzK7pN7PyObdbwlIFJbHYk8ofvr0zrU8SkV1rSi87KAHhWCdLGw1Q==
-
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -8029,6 +8132,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+napi-build-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
+ integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
+
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -8330,7 +8438,7 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
-"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -9359,6 +9467,28 @@ prebuild-install@^2.3.0:
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
+prebuild-install@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.2.1.tgz#87ba8cf17c65360a75eefeb3519e87973bf9791d"
+ integrity sha512-9DAccsInWHB48TBQi2eJkLPE049JuAI6FjIH0oIrij4bpDVEbX6JvlWRAcAAlUqBHhjgq0jNqA3m3bBXWm9v6w==
+ dependencies:
+ detect-libc "^1.0.3"
+ expand-template "^1.0.2"
+ github-from-package "0.0.0"
+ minimist "^1.2.0"
+ mkdirp "^0.5.1"
+ napi-build-utils "^1.0.1"
+ node-abi "^2.2.0"
+ noop-logger "^0.1.1"
+ npmlog "^4.0.1"
+ os-homedir "^1.0.1"
+ pump "^2.0.1"
+ rc "^1.2.7"
+ simple-get "^2.7.0"
+ tar-fs "^1.13.0"
+ tunnel-agent "^0.6.0"
+ which-pm-runs "^1.0.0"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -9718,6 +9848,16 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
react-dom@^15.5.4:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
@@ -10331,7 +10471,7 @@ resolve@^1.1.6, resolve@^1.4.0, resolve@^1.5.0:
dependencies:
path-parse "^1.0.5"
-resolve@^1.6.0:
+resolve@^1.6.0, resolve@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==
@@ -10386,6 +10526,88 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0"
inherits "^2.0.1"
+rollup-plugin-babel@3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.7.tgz#5b13611f1ab8922497e9d15197ae5d8a23fe3b1e"
+ integrity sha512-bVe2y0z/V5Ax1qU8NX/0idmzIwJPdUGu8Xx3vXH73h0yGjxfv2gkFI82MBVg49SlsFlLTBadBHb67zy4TWM3hA==
+ dependencies:
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-commonjs@9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89"
+ integrity sha512-0RM5U4Vd6iHjL6rLvr3lKBwnPsaVml+qxOGaaNUWN1lSq6S33KhITOfHmvxV3z2vy9Mk4t0g4rNlVaJJsNQPWA==
+ dependencies:
+ estree-walker "^0.5.2"
+ magic-string "^0.25.1"
+ resolve "^1.8.1"
+ rollup-pluginutils "^2.3.3"
+
+rollup-plugin-filesize@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-filesize/-/rollup-plugin-filesize-5.0.1.tgz#442a994465abf4f4f63ea20ac3267c3e0d05ee5d"
+ integrity sha512-zVUkEuJ543D86EaC5Ql2M6d6aAXwWbRwJ9NWSzTUS7F3vdd1cf+zlL+roQY8sW2hLIpbDMnGfev0dcy4bHQbjw==
+ dependencies:
+ boxen "^2.0.0"
+ brotli-size "0.0.3"
+ colors "^1.3.2"
+ deep-assign "^2.0.0"
+ filesize "^3.6.1"
+ gzip-size "^5.0.0"
+ terser "^3.10.0"
+
+rollup-plugin-node-resolve@3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz#908585eda12e393caac7498715a01e08606abc89"
+ integrity sha512-PJcd85dxfSBWih84ozRtBkB731OjXk0KnzN0oGp7WOWcarAFkVa71cV5hTJg2qpVsV2U8EUwrzHP3tvy9vS3qg==
+ dependencies:
+ builtin-modules "^2.0.0"
+ is-module "^1.0.0"
+ resolve "^1.1.6"
+
+rollup-plugin-replace@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.1.0.tgz#f9c07a4a89a2f8be912ee54b3f0f68d91e9ed0ae"
+ integrity sha512-SxrAIgpH/B5/W4SeULgreOemxcpEgKs2gcD42zXw50bhqGWmcnlXneVInQpAqzA/cIly4bJrOpeelmB9p4YXSQ==
+ dependencies:
+ magic-string "^0.25.1"
+ minimatch "^3.0.2"
+ rollup-pluginutils "^2.0.1"
+
+rollup-plugin-uglify@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.0.tgz#15aa8919e5cdc63b7cfc9319c781788b40084ce4"
+ integrity sha512-XtzZd159QuOaXNvcxyBcbUCSoBsv5YYWK+7ZwUyujSmISst8avRfjWlp7cGu8T2O52OJnpEBvl+D4WLV1k1iQQ==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ jest-worker "^23.2.0"
+ serialize-javascript "^1.5.0"
+ uglify-js "^3.4.9"
+
+rollup-pluginutils@^1.5.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
+ integrity sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794"
+ integrity sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA==
+ dependencies:
+ estree-walker "^0.5.2"
+ micromatch "^2.3.11"
+
+rollup@0.67.0:
+ version "0.67.0"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.67.0.tgz#16d4f259c55224dded6408e7666b7731500797a3"
+ integrity sha512-p34buXxArhwv9ieTdHvdhdo65Cbig68s/Z8llbZuiX5e+3zCqnBF02Ck9IH0tECrmvvrJVMws32Ry84hTnS1Tw==
+ dependencies:
+ "@types/estree" "0.0.39"
+ "@types/node" "*"
+
rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
@@ -10643,6 +10865,11 @@ serialize-javascript@^1.4.0:
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005"
integrity sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=
+serialize-javascript@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
+ integrity sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==
+
serve-index@^1.7.2:
version "1.9.1"
resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
@@ -10932,6 +11159,14 @@ source-map-support@^0.5.6:
buffer-from "^1.0.0"
source-map "^0.6.0"
+source-map-support@~0.5.6:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+ integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
source-map-url@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
@@ -10966,6 +11201,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+sourcemap-codec@^1.4.1:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.3.tgz#0ba615b73ec35112f63c2f2d9e7c3f87282b0e33"
+ integrity sha512-vFrY/x/NdsD7Yc8mpTJXuao9S8lq08Z/kOITHz6b7YbfI9xL8Spe5EvSQUHOI7SbpY8bRPr0U3kKSsPuqEGSfA==
+
spdx-correct@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
@@ -11498,6 +11738,15 @@ term-size@^1.2.0:
dependencies:
execa "^0.7.0"
+terser@^3.10.0:
+ version "3.10.11"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.10.11.tgz#e063da74b194dde9faf0a561f3a438c549d2da3f"
+ integrity sha512-iruZ7j14oBbRYJC5cP0/vTU7YOWjN+J1ZskEGoF78tFzXdkK2hbCL/3TRZN8XB+MuvFhvOHMp7WkOCBO4VEL5g==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+ source-map-support "~0.5.6"
+
test-exclude@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa"
@@ -11840,6 +12089,14 @@ uglify-js@^2.6, uglify-js@^2.8.29:
optionalDependencies:
uglify-to-browserify "~1.0.0"
+uglify-js@^3.4.9:
+ version "3.4.9"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
+ integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+
uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"