-
Notifications
You must be signed in to change notification settings - Fork 516
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(connetConfigure): add a connector to create a connector widget
* refactor(lib): extract `enhanceConfiguration` into utils * feat(connectors): add `connectConfigure` * feat(widgets): use `connectConfigure` * docs(dev-novel): add configure widget example * fix(configure): stick to the actual API * fix(connectConfigure): remove old searchParameters on refine * test(configure): move tests to connector * fix(connectConfigure): typos * test(connectConfigure): split bad usage * fix(connectConfigure): check usage for renderFn before * refactor(connectConfigure): review comments * refactor(configure): provide implicit undefined * test(connectConfigure): expect to throw on bad usage * test(connectConfigure): use `refine` from renderFn params * fix(connnectConfigure): typo on searchParameters * refactor(enhanceConfigure): export it from InstantSearch.js * test(enhanceConfiguration): unit testing
- Loading branch information
Showing
9 changed files
with
335 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* eslint-disable import/default */ | ||
|
||
import { storiesOf } from 'dev-novel'; | ||
|
||
import instantsearch from '../../../../index'; | ||
import { wrapWithHits } from '../../utils/wrap-with-hits.js'; | ||
|
||
const stories = storiesOf('Configure'); | ||
|
||
export default () => { | ||
stories.add( | ||
'Force 1 hit per page', | ||
wrapWithHits(container => { | ||
const description = document.createElement('div'); | ||
description.innerHTML = ` | ||
<p>Search parameters provied to the Configure widget:</p> | ||
<pre>{ hitsPerPage: 1 }</pre> | ||
`; | ||
|
||
container.appendChild(description); | ||
|
||
window.search.addWidget( | ||
instantsearch.widgets.configure({ | ||
hitsPerPage: 1, | ||
}) | ||
); | ||
}) | ||
); | ||
}; |
91 changes: 91 additions & 0 deletions
91
src/connectors/configure/__tests__/connectConfigure-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import algoliasearchHelper, { SearchParameters } from 'algoliasearch-helper'; | ||
|
||
import connectConfigure from '../connectConfigure.js'; | ||
|
||
const fakeClient = { addAlgoliaAgent: () => {}, search: jest.fn() }; | ||
|
||
describe('connectConfigure', () => { | ||
let helper; | ||
|
||
beforeEach(() => { | ||
helper = algoliasearchHelper(fakeClient, '', {}); | ||
}); | ||
|
||
describe('throws on bad usage', () => { | ||
it('without searchParameters', () => { | ||
const makeWidget = connectConfigure(); | ||
expect(() => makeWidget()).toThrow(); | ||
}); | ||
|
||
it('with a renderFn but no unmountFn', () => { | ||
expect(() => connectConfigure(jest.fn(), undefined)).toThrow(); | ||
}); | ||
|
||
it('with a unmountFn but no renderFn', () => { | ||
expect(() => connectConfigure(undefined, jest.fn())).toThrow(); | ||
}); | ||
}); | ||
|
||
it('should apply searchParameters', () => { | ||
const makeWidget = connectConfigure(); | ||
const widget = makeWidget({ searchParameters: { analytics: true } }); | ||
|
||
const config = widget.getConfiguration(SearchParameters.make({})); | ||
expect(config).toEqual({ analytics: true }); | ||
}); | ||
|
||
it('should apply searchParameters with a higher priority', () => { | ||
const makeWidget = connectConfigure(); | ||
const widget = makeWidget({ searchParameters: { analytics: true } }); | ||
|
||
{ | ||
const config = widget.getConfiguration( | ||
SearchParameters.make({ analytics: false }) | ||
); | ||
expect(config).toEqual({ analytics: true }); | ||
} | ||
|
||
{ | ||
const config = widget.getConfiguration( | ||
SearchParameters.make({ analytics: false, extra: true }) | ||
); | ||
expect(config).toEqual({ analytics: true }); | ||
} | ||
}); | ||
|
||
it('should apply new searchParameters on refine()', () => { | ||
const renderFn = jest.fn(); | ||
const makeWidget = connectConfigure(renderFn, jest.fn()); | ||
const widget = makeWidget({ searchParameters: { analytics: true } }); | ||
|
||
helper.setState(widget.getConfiguration()); | ||
widget.init({ helper }); | ||
|
||
expect(widget.getConfiguration()).toEqual({ analytics: true }); | ||
expect(helper.getState().analytics).toEqual(true); | ||
|
||
const { refine } = renderFn.mock.calls[0][0]; | ||
expect(refine).toBe(widget._refine); | ||
|
||
refine({ hitsPerPage: 3 }); | ||
|
||
expect(widget.getConfiguration()).toEqual({ hitsPerPage: 3 }); | ||
expect(helper.getState().analytics).toBe(undefined); | ||
expect(helper.getState().hitsPerPage).toBe(3); | ||
}); | ||
|
||
it('should dispose all the state set by configure', () => { | ||
const makeWidget = connectConfigure(); | ||
const widget = makeWidget({ searchParameters: { analytics: true } }); | ||
|
||
helper.setState(widget.getConfiguration()); | ||
widget.init({ helper }); | ||
|
||
expect(widget.getConfiguration()).toEqual({ analytics: true }); | ||
expect(helper.getState().analytics).toBe(true); | ||
|
||
const nextState = widget.dispose({ state: helper.getState() }); | ||
|
||
expect(nextState.analytics).toBe(undefined); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import isFunction from 'lodash/isFunction'; | ||
import isPlainObject from 'lodash/isPlainObject'; | ||
|
||
import { enhanceConfiguration } from '../../lib/InstantSearch.js'; | ||
|
||
const usage = `Usage: | ||
var customConfigureWidget = connectConfigure( | ||
function renderFn(params, isFirstRendering) { | ||
// params = { | ||
// refine, | ||
// widgetParams | ||
// } | ||
}, | ||
function disposeFn() {} | ||
) | ||
`; | ||
|
||
/** | ||
* @typedef {Object} CustomConfigureWidgetOptions | ||
* @property {Object} searchParameters The Configure widget options are search parameters | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} ConfigureRenderingOptions | ||
* @property {function(searchParameters: Object)} refine Sets new `searchParameters` and trigger a search. | ||
* @property {Object} widgetParams All original `CustomConfigureWidgetOptions` forwarded to the `renderFn`. | ||
*/ | ||
|
||
/** | ||
* The **Configure** connector provides the logic to build a custom widget | ||
* that will give you ability to override or force some search parameters sent to Algolia API. | ||
* | ||
* @type {Connector} | ||
* @param {function(ConfigureRenderingOptions)} renderFn Rendering function for the custom **Configure** Widget. | ||
* @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); | ||
} | ||
|
||
return (widgetParams = {}) => { | ||
if (!isPlainObject(widgetParams.searchParameters)) { | ||
throw new Error(usage); | ||
} | ||
|
||
return { | ||
getConfiguration() { | ||
return widgetParams.searchParameters; | ||
}, | ||
|
||
init({ helper }) { | ||
this._refine = this.refine(helper); | ||
|
||
if (isFunction(renderFn)) { | ||
renderFn( | ||
{ | ||
refine: this._refine, | ||
widgetParams, | ||
}, | ||
true | ||
); | ||
} | ||
}, | ||
|
||
refine(helper) { | ||
return searchParameters => { | ||
// merge new `searchParameters` with the ones set from other widgets | ||
const actualState = this.removeSearchParameters(helper.getState()); | ||
const nextSearchParameters = enhanceConfiguration({})(actualState, { | ||
getConfiguration: () => searchParameters, | ||
}); | ||
|
||
// trigger a search with the new merged searchParameters | ||
helper.setState(nextSearchParameters).search(); | ||
|
||
// update original `widgetParams.searchParameters` to the new refined one | ||
widgetParams.searchParameters = searchParameters; | ||
}; | ||
}, | ||
|
||
render() { | ||
if (renderFn) { | ||
renderFn( | ||
{ | ||
refine: this._refine, | ||
widgetParams, | ||
}, | ||
false | ||
); | ||
} | ||
}, | ||
|
||
dispose({ state }) { | ||
if (unmountFn) unmountFn(); | ||
return this.removeSearchParameters(state); | ||
}, | ||
|
||
removeSearchParameters(state) { | ||
// widgetParams are assumed 'controlled', | ||
// so they override whatever other widgets give the state | ||
return state.mutateMe(mutableState => { | ||
Object.keys(widgetParams.searchParameters).forEach(key => { | ||
delete mutableState[key]; | ||
}); | ||
}); | ||
}, | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { enhanceConfiguration } from '../InstantSearch'; | ||
|
||
const createWidget = (configuration = {}) => ({ | ||
getConfiguration: () => configuration, | ||
}); | ||
|
||
describe('enhanceConfiguration', () => { | ||
it('should return the same object if widget does not provide a configuration', () => { | ||
const configuration = { analytics: true, page: 2 }; | ||
const widget = {}; | ||
|
||
const output = enhanceConfiguration({})(configuration, widget); | ||
expect(output).toBe(configuration); | ||
}); | ||
|
||
it('should return a new object if widget does provide a configuration', () => { | ||
const configuration = { analytics: true, page: 2 }; | ||
const widget = createWidget(configuration); | ||
|
||
const output = enhanceConfiguration({})(configuration, widget); | ||
expect(output).not.toBe(configuration); | ||
}); | ||
|
||
it('should add widget configuration to an empty state', () => { | ||
const configuration = { analytics: true, page: 2 }; | ||
const widget = createWidget(configuration); | ||
|
||
const output = enhanceConfiguration({})(configuration, widget); | ||
expect(output).toEqual(configuration); | ||
}); | ||
|
||
it('should call `getConfiguration` from widget correctly', () => { | ||
const widget = { getConfiguration: jest.fn() }; | ||
|
||
const configuration = {}; | ||
const searchParametersFromUrl = {}; | ||
enhanceConfiguration(searchParametersFromUrl)(configuration, widget); | ||
|
||
expect(widget.getConfiguration).toHaveBeenCalled(); | ||
expect(widget.getConfiguration).toHaveBeenCalledWith( | ||
configuration, | ||
searchParametersFromUrl | ||
); | ||
}); | ||
|
||
it('should replace boolean values', () => { | ||
const actualConfiguration = { analytics: false }; | ||
const widget = createWidget({ analytics: true }); | ||
|
||
const output = enhanceConfiguration({})(actualConfiguration, widget); | ||
expect(output.analytics).toBe(true); | ||
}); | ||
|
||
it('should union array', () => { | ||
{ | ||
const actualConfiguration = { refinements: ['foo'] }; | ||
const widget = createWidget({ refinements: ['foo', 'bar'] }); | ||
|
||
const output = enhanceConfiguration({})(actualConfiguration, widget); | ||
expect(output.refinements).toEqual(['foo', 'bar']); | ||
} | ||
|
||
{ | ||
const actualConfiguration = { refinements: ['foo'] }; | ||
const widget = createWidget({ refinements: ['bar'] }); | ||
|
||
const output = enhanceConfiguration({})(actualConfiguration, widget); | ||
expect(output.refinements).toEqual(['foo', 'bar']); | ||
} | ||
|
||
{ | ||
const actualConfiguration = { refinements: ['foo', 'bar'] }; | ||
const widget = createWidget({ refinements: [] }); | ||
|
||
const output = enhanceConfiguration({})(actualConfiguration, widget); | ||
expect(output.refinements).toEqual(['foo', 'bar']); | ||
} | ||
}); | ||
|
||
it('should replace nested values', () => { | ||
const actualConfiguration = { refinements: { lvl1: ['foo'], lvl2: false } }; | ||
const widget = createWidget({ refinements: { lvl1: ['bar'], lvl2: true } }); | ||
|
||
const output = enhanceConfiguration({})(actualConfiguration, widget); | ||
expect(output).toEqual({ | ||
refinements: { lvl1: ['foo', 'bar'], lvl2: true }, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.