-
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(connectors): add connectAutocomplete (#2841)
* feat(connectors): add connectAutocomplete * docs(dev-novel): add autcomplete example * test(connectors): connectAutocomplete * docs(autocomplete): story with action on selected item * docs(connectAutocomplete): JS Doc * refactor(connectAutocomplete): default `indices` to `[]` * fix(connectAutocomplete): use `label` as find key this allow the usage of more than once index, for instance you can imagines cases where the user pass the same index twice with different search parameters applied cf: #2841 (comment) * fix(connectAutocomplete): call `.detach()` on derived indices * docs(devnovel): clearOptions on autocomplete before render * feat(connectAutocomplete): default `indices[x].hits` to an empty array * test(connectAutocomplete): use renderFn params * fix(connectAutocomplete): default hits to [] * docs(connectAutocomplete): story with multi-index * fix(connectAutocomplete): provide `currentRefinement` * docs(aucomplete): <em> query when no resutls * fix(connectAutocomplete): check if `results` and `results.htis` are present * docs(multi-index): add search box * docs(connectAutocomplete): specify the fact you get the main index * feat(connectAutocomplete): remove `helper` from public indices Fix #2313
- Loading branch information
Showing
6 changed files
with
455 additions
and
0 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,208 @@ | ||
/* eslint-disable import/default */ | ||
|
||
import { action, storiesOf } from 'dev-novel'; | ||
|
||
import instantsearch from '../../../../index.js'; | ||
import { wrapWithHitsAndJquery } from '../../utils/wrap-with-hits.js'; | ||
|
||
const stories = storiesOf('Autocomplete'); | ||
|
||
// Widget to search into brands, select one and set it as query | ||
const autocompleteBrands = instantsearch.connectors.connectAutocomplete( | ||
({ indices, refine, widgetParams: { containerNode } }, isFirstRendering) => { | ||
if (isFirstRendering) { | ||
containerNode.html(` | ||
<strong>Search for a brand:</strong> | ||
<select id="ais-autocomplete"></select> | ||
`); | ||
|
||
containerNode.find('select').selectize({ | ||
options: [], | ||
|
||
valueField: 'brand', | ||
labelField: 'brand', | ||
searchField: 'brand', | ||
|
||
highlight: false, | ||
|
||
onType: refine, | ||
|
||
onChange: refine, | ||
}); | ||
} | ||
|
||
if (!isFirstRendering && indices[0].results) { | ||
const autocompleteInstance = containerNode.find('select')[0].selectize; | ||
|
||
indices[0].results.hits.forEach(h => autocompleteInstance.addOption(h)); | ||
autocompleteInstance.refreshOptions(autocompleteInstance.isOpen); | ||
} | ||
} | ||
); | ||
|
||
// widget to search into hits, select a choice open a new page (event example) | ||
const autocompleteAndSelect = instantsearch.connectors.connectAutocomplete( | ||
({ indices, refine, widgetParams: { containerNode } }, isFirstRendering) => { | ||
const onItemSelected = objectID => { | ||
const item = indices.reduce((match, index) => { | ||
if (match) return match; | ||
return index.hits.find(obj => obj.objectID === objectID); | ||
}, null); | ||
|
||
action('item:selected')(item); | ||
}; | ||
|
||
if (isFirstRendering) { | ||
containerNode.html(` | ||
<strong>Search for anything:</strong> | ||
<select id="ais-autocomplete"></select> | ||
`); | ||
|
||
containerNode.find('select').selectize({ | ||
options: [], | ||
|
||
valueField: 'objectID', | ||
labelField: 'name', | ||
searchField: ['name', 'brand', 'categories', 'description'], | ||
|
||
render: { | ||
option: item => ` | ||
<div class="hit"> | ||
<div class="hit-picture"> | ||
<img src="${item.image}" /> | ||
</div> | ||
<div class="hit-content"> | ||
<div> | ||
<span>${item._highlightResult.name.value}</span> | ||
<span>${item.price_formatted}</span> | ||
<span>${item.rating} stars</span> | ||
</div> | ||
<div class="hit-type"> | ||
${item._highlightResult.type.value} | ||
</div> | ||
<div class="hit-description"> | ||
${item._highlightResult.description.value} | ||
</div> | ||
</div> | ||
</div> | ||
`, | ||
}, | ||
|
||
highlight: false, | ||
onType: refine, | ||
|
||
onChange: onItemSelected, | ||
}); | ||
|
||
// HACK: bind `autocompleteInstance.search` with an empty query so it returns | ||
// all the hits sent by Algolia | ||
const autocompleteInstance = containerNode.find('select')[0].selectize; | ||
autocompleteInstance.search.bind(autocompleteInstance, ''); | ||
} | ||
|
||
if (!isFirstRendering && indices[0].results) { | ||
const autocompleteInstance = containerNode.find('select')[0].selectize; | ||
|
||
// first clear options | ||
autocompleteInstance.clearOptions(); | ||
// add new ones | ||
indices[0].results.hits.forEach(h => autocompleteInstance.addOption(h)); | ||
// refresh the view | ||
autocompleteInstance.refreshOptions(autocompleteInstance.isOpen); | ||
} | ||
} | ||
); | ||
|
||
const multiIndex = instantsearch.connectors.connectAutocomplete( | ||
( | ||
{ indices, currentRefinement, widgetParams: { containerNode } }, | ||
isFirstRendering | ||
) => { | ||
if (isFirstRendering) { | ||
containerNode.append(` | ||
<div style="width: 100%"> | ||
<div | ||
id="hits0" | ||
style="width: 45%; margin-right: 5%; float: left;" | ||
> | ||
</div> | ||
<div | ||
id="hits1" | ||
style="width: 50%; float: right" | ||
> | ||
</div> | ||
<div style="clear: both;"></div> | ||
</div> | ||
`); | ||
} | ||
|
||
// display hits | ||
indices.forEach(({ hits }, index) => { | ||
const hitsHTML = | ||
hits.length === 0 | ||
? `No results for query <em>${currentRefinement}</em>` | ||
: hits.map( | ||
hit => ` | ||
<div class="hit"> | ||
<div class="hit-picture"> | ||
<img src="${hit.image}" /> | ||
</div> | ||
<div class="hit-content"> | ||
<div> | ||
<span>${hit._highlightResult.name.value}</span> | ||
</div> | ||
<div class="hit-type"> | ||
${hit._highlightResult.type.value} | ||
</div> | ||
</div> | ||
</div> | ||
` | ||
); | ||
|
||
containerNode.find(`#hits${index}`).html(hitsHTML); | ||
}); | ||
} | ||
); | ||
|
||
export default () => { | ||
stories | ||
.add( | ||
'default', | ||
wrapWithHitsAndJquery(containerNode => { | ||
window.search.addWidget(autocompleteBrands({ containerNode })); | ||
}) | ||
) | ||
.add( | ||
'Autcomplete into hits', | ||
wrapWithHitsAndJquery(containerNode => | ||
window.search.addWidget(autocompleteAndSelect({ containerNode })) | ||
) | ||
) | ||
.add( | ||
'Multi index', | ||
wrapWithHitsAndJquery(containerNode => { | ||
containerNode.append('<div id="multi-index-search-box"></div>'); | ||
window.search.addWidget( | ||
instantsearch.widgets.searchBox({ | ||
container: '#multi-index-search-box', | ||
placeholder: 'Search into the two indices', | ||
poweredBy: false, | ||
autofocus: false, | ||
}) | ||
); | ||
window.search.addWidget( | ||
multiIndex({ | ||
containerNode, | ||
indices: [{ label: 'ikea', value: 'ikea' }], | ||
}) | ||
); | ||
}) | ||
); | ||
}; |
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
72 changes: 72 additions & 0 deletions
72
src/connectors/autocomplete/__tests__/connectAutocomplete-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,72 @@ | ||
import jsHelper from 'algoliasearch-helper'; | ||
import connectAutocomplete from '../connectAutocomplete.js'; | ||
|
||
const fakeClient = { addAlgoliaAgent: () => {} }; | ||
|
||
describe('connectAutocomplete', () => { | ||
it('throws without `renderFn`', () => { | ||
expect(() => connectAutocomplete()).toThrow(); | ||
}); | ||
|
||
it('renders during init and render', () => { | ||
const renderFn = jest.fn(); | ||
const makeWidget = connectAutocomplete(renderFn); | ||
const widget = makeWidget(); | ||
|
||
expect(renderFn).toHaveBeenCalledTimes(0); | ||
|
||
const helper = jsHelper(fakeClient, '', {}); | ||
helper.search = jest.fn(); | ||
|
||
widget.init({ | ||
helper, | ||
instantSearchInstance: {}, | ||
}); | ||
|
||
expect(renderFn).toHaveBeenCalledTimes(1); | ||
expect(renderFn.mock.calls[0][1]).toBeTruthy(); | ||
|
||
widget.render({ | ||
widgetParams: {}, | ||
indices: widget.indices, | ||
instantSearchInstance: widget.instantSearchInstance, | ||
}); | ||
|
||
expect(renderFn).toHaveBeenCalledTimes(2); | ||
expect(renderFn.mock.calls[1][1]).toBeFalsy(); | ||
}); | ||
|
||
it('creates derived helper', () => { | ||
const renderFn = jest.fn(); | ||
const makeWidget = connectAutocomplete(renderFn); | ||
const widget = makeWidget({ indices: [{ label: 'foo', value: 'foo' }] }); | ||
|
||
const helper = jsHelper(fakeClient, '', {}); | ||
helper.search = jest.fn(); | ||
|
||
widget.init({ helper, instantSearchInstance: {} }); | ||
expect(renderFn).toHaveBeenCalledTimes(1); | ||
|
||
// original helper + derived one | ||
const renderOpts = renderFn.mock.calls[0][0]; | ||
expect(renderOpts.indices).toHaveLength(2); | ||
}); | ||
|
||
it('set a query and trigger search on `refine`', () => { | ||
const renderFn = jest.fn(); | ||
const makeWidget = connectAutocomplete(renderFn); | ||
const widget = makeWidget(); | ||
|
||
const helper = jsHelper(fakeClient, '', {}); | ||
helper.search = jest.fn(); | ||
|
||
widget.init({ helper, instantSearchInstance: {} }); | ||
|
||
const { refine } = renderFn.mock.calls[0][0]; | ||
refine('foo'); | ||
|
||
expect(refine).toBe(widget._refine); | ||
expect(helper.search).toHaveBeenCalledTimes(1); | ||
expect(helper.getState().query).toBe('foo'); | ||
}); | ||
}); |
Oops, something went wrong.