Skip to content

Commit

Permalink
feat(connector): connectHits (iteration 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Stanislawski committed Mar 21, 2017
1 parent 7350c3a commit bca09af
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 107 deletions.
8 changes: 0 additions & 8 deletions src/connectors/hits/__tests__/connectHits-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import sinon from 'sinon';

import jsHelper from 'algoliasearch-helper';
Expand Down Expand Up @@ -42,9 +40,6 @@ describe('connectHits', () => {
// test if isFirstRendering is true during init
expect(rendering.lastCall.args[1]).toBe(true);

const firstRenderingOptions = rendering.lastCall.args[0];
expect(firstRenderingOptions.containerNode).toBe(container);

widget.render({
results: new SearchResults(helper.state, [{}]),
state: helper.state,
Expand All @@ -55,9 +50,6 @@ describe('connectHits', () => {
// test that rendering has been called during init with isFirstRendering = false
expect(rendering.callCount).toBe(2);
expect(rendering.lastCall.args[1]).toBe(false);

const secondRenderingOptions = rendering.lastCall.args[0];
expect(secondRenderingOptions.containerNode).toBe(container);
});

it('Provides the hits and the whole results', () => {
Expand Down
82 changes: 6 additions & 76 deletions src/connectors/hits/connectHits.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,21 @@
import {
bemHelper,
prepareTemplateProps,
getContainerNode,
} from '../../lib/utils.js';
import cx from 'classnames';
import defaultTemplates from './defaultTemplates.js';

const bem = bemHelper('ais-hits');

/**
* Display the list of results (hits) from the current search
* @function hits
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {number} [options.hitsPerPage=20] The number of hits to display per page [*]
* @param {Object} [options.templates] Templates to use for the widget
* @param {string|Function} [options.templates.empty=''] Template to use when there are no results.
* @param {string|Function} [options.templates.item=''] Template to use for each result. This template will receive an object containing a single record.
* @param {string|Function} [options.templates.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.
* @param {Object} [options.transformData] Method to change the object passed to the templates
* @param {Function} [options.transformData.empty] Method used to change the object passed to the `empty` template
* @param {Function} [options.transformData.item] Method used to change the object passed to the `item` template
* @param {Function} [options.transformData.allItems] Method used to change the object passed to the `allItems` template
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the wrapping element
* @param {string|string[]} [options.cssClasses.empty] CSS class to add to the wrapping element when no results
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each result
* @return {Object}
*/
const usage = `
Usage:
hits({
container,
[ cssClasses.{root,empty,item}={} ],
[ templates.{empty,item} | templates.{empty, allItems} ],
[ transformData.{empty,item} | transformData.{empty, allItems} ],
[ hitsPerPage=20 ]
})`;
const connectHits = renderHits => ({
container,
cssClasses: userCssClasses = {},
templates = defaultTemplates,
transformData,
hitsPerPage = 20,
} = {}) => {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
}

if (templates.item && templates.allItems) {
throw new Error(`Must contain only allItems OR item template.${usage}`);
}

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),
};

return {
} = {}) => ({
getConfiguration: () => ({hitsPerPage}),
init({templatesConfig}) {
this._templateProps = prepareTemplateProps({
transformData,
defaultTemplates,
templatesConfig,
templates,
});

init({instantSearchInstance}) {
renderHits({
cssClasses,
hits: [],
results: undefined,
templateProps: this._templateProps,
containerNode,
instantSearchInstance,
}, true);
},
render({results}) {
render({results, instantSearchInstance}) {
renderHits({
cssClasses,
hits: results.hits,
results,
templateProps: this._templateProps,
containerNode,
instantSearchInstance,
}, false);
},
};
};
});

export default connectHits;
10 changes: 5 additions & 5 deletions src/widgets/hits/__tests__/hits-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import expect from 'expect';
import sinon from 'sinon';
import expectJSX from 'expect-jsx';
expect.extend(expectJSX);
import hits from '../hits';
import Hits from '../../../components/Hits';
import defaultTemplates from '../../../connectors/hits/defaultTemplates.js';
import hits from '../hits.js';
import Hits from '../../../components/Hits.js';
import defaultTemplates from '../defaultTemplates.js';

describe('hits call', () => {
it('throws an exception when no container', () => {
expect(hits).toThrow(/^Must provide a container/);
expect(hits).toThrow();
});
});

Expand All @@ -33,7 +33,7 @@ describe('hits()', () => {
useCustomCompileOptions: {item: false, empty: false},
};
widget = hits({container, cssClasses: {root: ['root', 'cx']}});
widget.init({});
widget.init({instantSearchInstance: {templateProps}});
results = {hits: [{first: 'hit', second: 'hit'}]};
});

Expand Down
File renamed without changes.
108 changes: 90 additions & 18 deletions src/widgets/hits/hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ import Hits from '../../components/Hits.js';

import connectHits from '../../connectors/hits/connectHits.js';

import {
bemHelper,
prepareTemplateProps,
getContainerNode,
} from '../../lib/utils.js';
import cx from 'classnames';
import defaultTemplates from './defaultTemplates.js';

const bem = bemHelper('ais-hits');

const renderer = ({
renderState,
cssClasses,
containerNode,
transformData,
templates,
}) => ({
hits, // eslint-disable-line
results,
templateProps,
instantSearchInstance,
}, isFirstRendering) => {
if (isFirstRendering) {
renderState.templateProps = prepareTemplateProps({
transformData,
defaultTemplates,
templatesConfig: instantSearchInstance.templatesConfig,
templates,
});
return;
}

ReactDOM.render(
<Hits
cssClasses={cssClasses}
hits={hits}
results={results}
templateProps={renderState.templateProps}
/>,
containerNode
);
};

/**
* Display the list of results (hits) from the current search
* @function hits
Expand All @@ -23,23 +66,52 @@ import connectHits from '../../connectors/hits/connectHits.js';
* @param {string|string[]} [options.cssClasses.item] CSS class to add to each result
* @return {Object}
*/
export default connectHits(defaultRendering);
const usage = `
Usage:
hits({
container,
[ cssClasses.{root,empty,item}={} ],
[ templates.{empty,item} | templates.{empty, allItems} ],
[ transformData.{empty,item} | transformData.{empty, allItems} ],
[ hitsPerPage=20 ]
})`;

function defaultRendering({
cssClasses,
hits,
results,
templateProps,
containerNode,
}, isFirstRendering) {
if (isFirstRendering) return;
ReactDOM.render(
<Hits
cssClasses={cssClasses}
hits={hits}
results={results}
templateProps={templateProps}
/>,
containerNode
);
export default function hits({
container,
cssClasses: userCssClasses = {},
templates = defaultTemplates,
transformData,
hitsPerPage = 20,
}) {
if (!container) {
throw new Error(`Must provide a container.${usage}`);
}

if (templates.item && templates.allItems) {
throw new Error(`Must contain only allItems OR item template.${usage}`);
}

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),
};

const specializedRenderer = renderer({
containerNode,
cssClasses,
renderState: {},
transformData,
templates,
});

try {
const makeHits = connectHits(specializedRenderer);
return makeHits({
hitsPerPage,
});
} catch (e) {
throw new Error(usage);
}
}

0 comments on commit bca09af

Please sign in to comment.