Skip to content

Commit

Permalink
feat(connector): stats connector
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Stanislawski committed Feb 15, 2017
1 parent 9996b4d commit 680743b
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 89 deletions.
8 changes: 5 additions & 3 deletions src/components/Stats/Stats.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react';

import Template from '../Template.js';
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
import headerFooterHOC from '../../decorators/headerFooter.js';

class Stats extends React.Component {
export class RawStats extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.nbHits !== nextProps.hits ||
this.props.processingTimeMS !== nextProps.processingTimeMS;
Expand All @@ -28,7 +30,7 @@ class Stats extends React.Component {
}
}

Stats.propTypes = {
RawStats.propTypes = {
cssClasses: React.PropTypes.shape({
time: React.PropTypes.string,
}),
Expand All @@ -41,4 +43,4 @@ Stats.propTypes = {
templateProps: React.PropTypes.object.isRequired,
};

export default Stats;
export default autoHideContainerHOC(headerFooterHOC(RawStats));
2 changes: 1 addition & 1 deletion src/components/Stats/__tests__/Stats-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React from 'react';
import expect from 'expect';
import TestUtils from 'react-addons-test-utils';
import Stats from '../Stats';
import {RawStats as Stats} from '../Stats';
import Template from '../../Template';

import expectJSX from 'expect-jsx';
Expand Down
102 changes: 102 additions & 0 deletions src/connectors/stats/connectStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
bemHelper,
prepareTemplateProps,
getContainerNode,
} from '../../lib/utils.js';
import cx from 'classnames';
import defaultTemplates from './defaultTemplates.js';

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

/**
* Display various stats about the current search state
* @function stats
* @param {string|DOMElement} options.container CSS Selector or DOMElement to insert the widget
* @param {Object} [options.templates] Templates to use for the widget
* @param {string|Function} [options.templates.header=''] Header template
* @param {string|Function} [options.templates.body] Body template, provided with `hasManyResults`,
* `hasNoResults`, `hasOneResult`, `hitsPerPage`, `nbHits`, `nbPages`, `page`, `processingTimeMS`, `query`
* @param {string|Function} [options.templates.footer=''] Footer template
* @param {Function} [options.transformData.body] Function to change the object passed to the `body` template
* @param {boolean} [options.autoHideContainer=true] Hide the container when no results match
* @param {Object} [options.cssClasses] CSS classes to add
* @param {string|string[]} [options.cssClasses.root] CSS class to add to the root element
* @param {string|string[]} [options.cssClasses.header] CSS class to add to the header element
* @param {string|string[]} [options.cssClasses.body] CSS class to add to the body element
* @param {string|string[]} [options.cssClasses.footer] CSS class to add to the footer element
* @param {string|string[]} [options.cssClasses.time] CSS class to add to the element wrapping the time processingTimeMs
* @return {Object}
*/
const usage = `Usage:
stats({
container,
[ templates.{header,body,footer} ],
[ transformData.{body} ],
[ autoHideContainer]
})`;
const connectStats = statsRendering => ({
container,
cssClasses: userCssClasses = {},
autoHideContainer = true,
templates = defaultTemplates,
collapsible = false,
transformData,
} = {}) => {
if (!container) throw new Error(usage);
const containerNode = getContainerNode(container);

if (!containerNode) {
throw new Error(usage);
}

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

return {
init({templatesConfig, helper}) {
this._templateProps = prepareTemplateProps({
transformData,
defaultTemplates,
templatesConfig,
templates,
});

statsRendering({
collapsible,
cssClasses,
hitsPerPage: helper.state.hitsPerPage,
nbHits: 0,
nbPages: 0,
page: helper.state.page,
processingTimeMS: -1,
query: '',
shouldAutoHideContainer: autoHideContainer,
templateProps: this._templateProps,
containerNode,
}, true);
},

render({results}) {
statsRendering({
collapsible,
cssClasses,
hitsPerPage: results.hitsPerPage,
nbHits: results.nbHits,
nbPages: results.nbPages,
page: results.page,
processingTimeMS: results.processingTimeMS,
query: results.query,
shouldAutoHideContainer: autoHideContainer && results.nbHits === 0,
templateProps: this._templateProps,
containerNode,
}, false);
},
};
};

export default connectStats;
File renamed without changes.
9 changes: 0 additions & 9 deletions src/widgets/stats/__tests__/stats-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,9 @@ describe('stats()', () => {
let widget;
let results;

let autoHideContainer;
let headerFooter;

beforeEach(() => {
ReactDOM = {render: sinon.spy()};
stats.__Rewire__('ReactDOM', ReactDOM);
autoHideContainer = sinon.stub().returns(Stats);
stats.__Rewire__('autoHideContainerHOC', autoHideContainer);
headerFooter = sinon.stub().returns(Stats);
stats.__Rewire__('headerFooterHOC', headerFooter);

container = document.createElement('div');
widget = stats({container, cssClasses: {body: ['body', 'cx']}});
Expand Down Expand Up @@ -70,8 +63,6 @@ describe('stats()', () => {
templateProps: ReactDOM.render.firstCall.args[0].props.templateProps,
};
expect(ReactDOM.render.calledTwice).toBe(true, 'ReactDOM.render called twice');
expect(autoHideContainer.calledOnce).toBe(true, 'autoHideContainer called once');
expect(headerFooter.calledOnce).toBe(true, 'headerFooter called once');
expect(ReactDOM.render.firstCall.args[0]).toEqualJSX(<Stats {...props} />);
expect(ReactDOM.render.firstCall.args[1]).toEqual(container);
expect(ReactDOM.render.secondCall.args[0]).toEqualJSX(<Stats {...props} />);
Expand Down
108 changes: 32 additions & 76 deletions src/widgets/stats/stats.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {
bemHelper,
prepareTemplateProps,
getContainerNode,
} from '../../lib/utils.js';
import autoHideContainerHOC from '../../decorators/autoHideContainer.js';
import headerFooterHOC from '../../decorators/headerFooter.js';
import StatsComponent from '../../components/Stats/Stats.js';
import cx from 'classnames';
import defaultTemplates from './defaultTemplates.js';

const bem = bemHelper('ais-stats');
import Stats from '../../components/Stats/Stats.js';
import connectStats from '../../connectors/stats/connectStats.js';

/**
* Display various stats about the current search state
Expand All @@ -32,69 +23,34 @@ const bem = bemHelper('ais-stats');
* @param {string|string[]} [options.cssClasses.time] CSS class to add to the element wrapping the time processingTimeMs
* @return {Object}
*/
const usage = `Usage:
stats({
container,
[ templates.{header,body,footer} ],
[ transformData.{body} ],
[ autoHideContainer]
})`;
function stats({
container,
cssClasses: userCssClasses = {},
autoHideContainer = true,
templates = defaultTemplates,
collapsible = false,
transformData,
} = {}) {
if (!container) throw new Error(usage);
const containerNode = getContainerNode(container);

let Stats = headerFooterHOC(StatsComponent);
if (autoHideContainer === true) {
Stats = autoHideContainerHOC(Stats);
}

if (!containerNode) {
throw new Error(usage);
}

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

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

render({results}) {
ReactDOM.render(
<Stats
collapsible={collapsible}
cssClasses={cssClasses}
hitsPerPage={results.hitsPerPage}
nbHits={results.nbHits}
nbPages={results.nbPages}
page={results.page}
processingTimeMS={results.processingTimeMS}
query={results.query}
shouldAutoHideContainer={results.nbHits === 0}
templateProps={this._templateProps}
/>,
containerNode
);
},
};
export default connectStats(defaultRendering);
function defaultRendering({
collapsible,
cssClasses,
hitsPerPage,
nbHits,
nbPages,
page,
processingTimeMS,
query,
shouldAutoHideContainer,
templateProps,
containerNode,
}, isFirstRendering) {
if (isFirstRendering) return;
ReactDOM.render(
<Stats
collapsible={collapsible}
cssClasses={cssClasses}
hitsPerPage={hitsPerPage}
nbHits={nbHits}
nbPages={nbPages}
page={page}
processingTimeMS={processingTimeMS}
query={query}
shouldAutoHideContainer={shouldAutoHideContainer}
templateProps={templateProps}
/>,
containerNode
);
}

export default stats;

0 comments on commit 680743b

Please sign in to comment.