diff --git a/src/connectors/star-rating/__tests__/connectStarRating-test.js b/src/connectors/star-rating/__tests__/connectStarRating-test.js index 6a46b470cb..2e98012efb 100644 --- a/src/connectors/star-rating/__tests__/connectStarRating-test.js +++ b/src/connectors/star-rating/__tests__/connectStarRating-test.js @@ -1,5 +1,3 @@ - - import sinon from 'sinon'; import jsHelper from 'algoliasearch-helper'; @@ -44,11 +42,8 @@ describe('connectStarRating', () => { expect(isFirstRendering).toBe(true); // should provide good values for the first rendering - const {containerNode, facetValues, collapsible, shouldAutoHideContainer} = rendering.lastCall.args[0]; - expect(containerNode).toBe(container); + const {facetValues} = rendering.lastCall.args[0]; expect(facetValues).toEqual([]); - expect(collapsible).toBe(false); - expect(shouldAutoHideContainer).toBe(true); } widget.render({ @@ -68,32 +63,25 @@ describe('connectStarRating', () => { expect(isFirstRendering).toBe(false); // should provide good values after the first search - const {containerNode, facetValues, collapsible, shouldAutoHideContainer} = rendering.lastCall.args[0]; - expect(containerNode).toBe(container); + const {facetValues} = rendering.lastCall.args[0]; expect(facetValues).toEqual([ { - count: 1000, isRefined: false, - labels: {andUp: '& Up'}, name: '4', + count: 1000, isRefined: false, name: '4', stars: [true, true, true, true, false], }, { - count: 1050, isRefined: false, - labels: {andUp: '& Up'}, name: '3', + count: 1050, isRefined: false, name: '3', stars: [true, true, true, false, false], }, { - count: 1070, isRefined: false, - labels: {andUp: '& Up'}, name: '2', + count: 1070, isRefined: false, name: '2', stars: [true, true, false, false, false], }, { - count: 1080, isRefined: false, - labels: {andUp: '& Up'}, name: '1', + count: 1080, isRefined: false, name: '1', stars: [true, false, false, false, false], }, ]); - expect(collapsible).toBe(false); - expect(shouldAutoHideContainer).toBe(false); } }); @@ -122,10 +110,10 @@ describe('connectStarRating', () => { { // first rendering const renderOptions = rendering.lastCall.args[0]; - const {toggleRefinement, facetValues} = renderOptions; + const {refine, facetValues} = renderOptions; expect(facetValues).toEqual([]); expect(helper.getRefinements(attributeName)).toEqual([]); - toggleRefinement('3'); + refine('3'); expect(helper.getRefinements(attributeName)).toEqual([ {type: 'disjunctive', value: '3'}, {type: 'disjunctive', value: '4'}, @@ -151,26 +139,22 @@ describe('connectStarRating', () => { { // Second rendering const renderOptions = rendering.lastCall.args[0]; - const {toggleRefinement, facetValues} = renderOptions; + const {refine, facetValues} = renderOptions; expect(facetValues).toEqual([ { - count: 1000, isRefined: false, - labels: {andUp: '& Up'}, name: '4', + count: 1000, isRefined: false, name: '4', stars: [true, true, true, true, false], }, { - count: 1050, isRefined: true, - labels: {andUp: '& Up'}, name: '3', + count: 1050, isRefined: true, name: '3', stars: [true, true, true, false, false], }, { - count: 1070, isRefined: false, - labels: {andUp: '& Up'}, name: '2', + count: 1070, isRefined: false, name: '2', stars: [true, true, false, false, false], }, { - count: 1080, isRefined: false, - labels: {andUp: '& Up'}, name: '1', + count: 1080, isRefined: false, name: '1', stars: [true, false, false, false, false], }, ]); @@ -179,7 +163,7 @@ describe('connectStarRating', () => { {type: 'disjunctive', value: '4'}, {type: 'disjunctive', value: '5'}, ]); - toggleRefinement('4'); + refine('4'); expect(helper.getRefinements(attributeName)).toEqual([ {type: 'disjunctive', value: '4'}, {type: 'disjunctive', value: '5'}, diff --git a/src/connectors/star-rating/connectStarRating.js b/src/connectors/star-rating/connectStarRating.js index 454fc589dc..b8560d155a 100644 --- a/src/connectors/star-rating/connectStarRating.js +++ b/src/connectors/star-rating/connectStarRating.js @@ -1,13 +1,4 @@ -import { - bemHelper, - prepareTemplateProps, - getContainerNode, -} from '../../lib/utils.js'; -import cx from 'classnames'; -import defaultTemplates from './defaultTemplates.js'; -import defaultLabels from './defaultLabels.js'; - -const bem = bemHelper('ais-star-rating'); +import {checkRendering} from '../../lib/utils.js'; /** * Instantiate a list of refinements based on a rating attribute @@ -42,144 +33,117 @@ const bem = bemHelper('ais-star-rating'); * @return {Object} */ 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 ] -})`; -const connectStarRating = starRatingRendering => ({ - container, +var customStarRating = connectStarRating(function render(params, isFirstRendering) { + // params = { + // facetValues, + // createURL, + // instantSearchInstance, + // results, + // } +}); +search.addWidget( + customStarRatingI({ + attributeName, + [ max=5 ], + }); +); +Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectStarRating.html +`; +export default function connectStarRating(renderFn) { + checkRendering(renderFn, usage); + + return ({ attributeName, max = 5, - cssClasses: userCssClasses = {}, - labels = defaultLabels, - templates = defaultTemplates, - collapsible = false, - transformData, - autoHideContainer = true, }) => { - const containerNode = getContainerNode(container); + if (!attributeName) { + throw new Error(usage); + } - if (!container || !attributeName) { - throw new Error(usage); - } + return { + getConfiguration() { + return {disjunctiveFacets: [attributeName]}; + }, - 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), - }; + init({helper, createURL, instantSearchInstance}) { + this._instantSearchInstance = instantSearchInstance; + this._toggleRefinement = this._toggleRefinement.bind(this, helper); + this._createURL = state => facetValue => createURL(state.toggleRefinement(attributeName, facetValue)); - return { - getConfiguration: () => ({disjunctiveFacets: [attributeName]}), + renderFn({ + instantSearchInstance: this._instantSearchInstance, + facetValues: [], + nbHits: 0, + refine: this._toggleRefinement, + createURL: this._createURL(helper.state), + }, true); + }, - init({templatesConfig, helper, createURL}) { - this._templateProps = prepareTemplateProps({ - transformData, - defaultTemplates, - templatesConfig, - templates, - }); - this._toggleRefinement = this._toggleRefinement.bind(this, helper); - this._createURL = state => facetValue => createURL(state.toggleRefinement(attributeName, facetValue)); - - starRatingRendering({ - collapsible, - createURL: this._createURL(helper.state), - cssClasses, - facetValues: [], - shouldAutoHideContainer: autoHideContainer, - templateProps: this._templateProps, - toggleRefinement: this._toggleRefinement, - containerNode, - }, true); - }, - - render({helper, results, state}) { - const facetValues = []; - const allValues = {}; - for (let v = max; v >= 0; --v) { - allValues[v] = 0; - } - results.getFacetValues(attributeName).forEach(facet => { - const val = Math.round(facet.name); - if (!val || val > max) { - return; - } - for (let v = val; v >= 1; --v) { - allValues[v] += facet.count; + render({helper, results, state}) { + const facetValues = []; + const allValues = {}; + for (let v = max; v >= 0; --v) { + allValues[v] = 0; } - }); - const refinedStar = this._getRefinedStar(helper); - for (let star = max - 1; star >= 1; --star) { - const count = allValues[star]; - if (refinedStar && star !== refinedStar && count === 0) { - // skip count==0 when at least 1 refinement is enabled - // eslint-disable-next-line no-continue - continue; - } - const stars = []; - for (let i = 1; i <= max; ++i) { - stars.push(i <= star); - } - facetValues.push({ - stars, - name: String(star), - count, - isRefined: refinedStar === star, - labels, + results.getFacetValues(attributeName).forEach(facet => { + const val = Math.round(facet.name); + if (!val || val > max) { + return; + } + for (let v = val; v >= 1; --v) { + allValues[v] += facet.count; + } }); - } + const refinedStar = this._getRefinedStar(helper); + for (let star = max - 1; star >= 1; --star) { + const count = allValues[star]; + if (refinedStar && star !== refinedStar && count === 0) { + // skip count==0 when at least 1 refinement is enabled + // eslint-disable-next-line no-continue + continue; + } + const stars = []; + for (let i = 1; i <= max; ++i) { + stars.push(i <= star); + } + facetValues.push({ + stars, + name: String(star), + count, + isRefined: refinedStar === star, + }); + } - starRatingRendering({ - collapsible, - createURL: this._createURL(state), - cssClasses, - facetValues, - shouldAutoHideContainer: autoHideContainer && results.nbHits === 0, - templateProps: this._templateProps, - toggleRefinement: this._toggleRefinement, - containerNode, - }, false); - }, + renderFn({ + instantSearchInstance: this._instantSearchInstance, + facetValues, + nbHits: results.nbHits, + refine: this._toggleRefinement, + createURL: this._createURL(state), + }, false); + }, - _toggleRefinement(helper, facetValue) { - const isRefined = this._getRefinedStar(helper) === Number(facetValue); - helper.clearRefinements(attributeName); - if (!isRefined) { - for (let val = Number(facetValue); val <= max; ++val) { - helper.addDisjunctiveFacetRefinement(attributeName, val); + _toggleRefinement(helper, facetValue) { + const isRefined = this._getRefinedStar(helper) === Number(facetValue); + helper.clearRefinements(attributeName); + if (!isRefined) { + for (let val = Number(facetValue); val <= max; ++val) { + helper.addDisjunctiveFacetRefinement(attributeName, val); + } } - } - helper.search(); - }, + helper.search(); + }, - _getRefinedStar(helper) { - let refinedStar = undefined; - const refinements = helper.getRefinements(attributeName); - refinements.forEach(r => { - if (!refinedStar || Number(r.value) < refinedStar) { - refinedStar = Number(r.value); - } - }); - return refinedStar; - }, + _getRefinedStar(helper) { + let refinedStar = undefined; + const refinements = helper.getRefinements(attributeName); + refinements.forEach(r => { + if (!refinedStar || Number(r.value) < refinedStar) { + refinedStar = Number(r.value); + } + }); + return refinedStar; + }, + }; }; -}; - -export default connectStarRating; +} diff --git a/src/widgets/star-rating/__tests__/star-rating-test.js b/src/widgets/star-rating/__tests__/star-rating-test.js index dcca2d37f7..b03a70897f 100644 --- a/src/widgets/star-rating/__tests__/star-rating-test.js +++ b/src/widgets/star-rating/__tests__/star-rating-test.js @@ -3,8 +3,8 @@ import sinon from 'sinon'; import expect from 'expect'; import expectJSX from 'expect-jsx'; expect.extend(expectJSX); -import defaultTemplates from '../../../connectors/star-rating/defaultTemplates.js'; -import defaultLabels from '../../../connectors/star-rating/defaultLabels.js'; +import defaultTemplates from '../../../widgets/star-rating/defaultTemplates.js'; +import defaultLabels from '../../../widgets/star-rating/defaultLabels.js'; import starRating from '../star-rating.js'; import RefinementList from '../../../components/RefinementList/RefinementList.js'; @@ -39,7 +39,7 @@ describe('starRating()', () => { hits: [], }; createURL = () => '#'; - widget.init({helper}); + widget.init({helper, instantSearchInstance: {templatesConfig: undefined}}); }); it('configures the underlying disjunctive facet', () => { diff --git a/src/connectors/star-rating/defaultLabels.js b/src/widgets/star-rating/defaultLabels.js similarity index 100% rename from src/connectors/star-rating/defaultLabels.js rename to src/widgets/star-rating/defaultLabels.js diff --git a/src/connectors/star-rating/defaultTemplates.js b/src/widgets/star-rating/defaultTemplates.js similarity index 100% rename from src/connectors/star-rating/defaultTemplates.js rename to src/widgets/star-rating/defaultTemplates.js diff --git a/src/widgets/star-rating/star-rating.js b/src/widgets/star-rating/star-rating.js index fda6c2c8fc..187e787df4 100644 --- a/src/widgets/star-rating/star-rating.js +++ b/src/widgets/star-rating/star-rating.js @@ -1,7 +1,74 @@ import React from 'react'; import ReactDOM from 'react-dom'; +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, + facetValues, + createURL, + instantSearchInstance: {templatesConfig}, + nbHits, +}, isFirstRendering) => { + if (isFirstRendering) { + renderState.templateProps = prepareTemplateProps({ + transformData, + defaultTemplates, + templatesConfig, + templates, + }); + return; + } + + const shouldAutoHideContainer = autoHideContainer && nbHits === 0; + + ReactDOM.render( + ({...facetValue, 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 ] +})`; /** * Instantiate a list of refinements based on a rating attribute @@ -33,30 +100,55 @@ import connectStarRating from '../../connectors/star-rating/connectStarRating.js * @param {string|string[]} [options.cssClasses.active] CSS class to add to each active element * @param {object|boolean} [options.collapsible=false] Hide the widget body and footer when clicking on header * @param {boolean} [options.collapsible.collapsed] Initial collapsed state of a collapsible widget - * @return {Object} + * @return {Object} widget */ -export default connectStarRating(defaultRendering); -function defaultRendering({ - collapsible, - createURL, - cssClasses, - facetValues, - shouldAutoHideContainer, - templateProps, - toggleRefinement, - containerNode, -}, isFirstRendering) { - if (isFirstRendering) return; - ReactDOM.render( - , - containerNode - ); +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); + return makeWidget({attributeName, max}); + } catch (e) { + throw new Error(usage); + } }