Skip to content

Commit

Permalink
feat(connectors): add connectAutocomplete (#2841)
Browse files Browse the repository at this point in the history
* 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
Maxime Janton authored and bobylito committed May 29, 2018
1 parent d1f99fb commit 4bec81e
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 0 deletions.
2 changes: 2 additions & 0 deletions dev/app/jquery/init-stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import initSortBySelectorStories from './stories/sort-by-selector.stories';
import initStarRatingStories from './stories/star-rating.stories';
import initStatsStories from './stories/stats.stories';
import initToggleStories from './stories/toggle.stories';
import initAutcompleteStories from './stories/autocomplete.stories';

export default () => {
initClearAllStories();
Expand All @@ -34,4 +35,5 @@ export default () => {
initStarRatingStories();
initStatsStories();
initToggleStories();
initAutcompleteStories();
};
208 changes: 208 additions & 0 deletions dev/app/jquery/stories/autocomplete.stories.js
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' }],
})
);
})
);
};
7 changes: 7 additions & 0 deletions dev/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<title>Instant search demo built with instantsearch.js</title>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=default,Array.from"></script>

<!-- jQuery -->
<script src="https://cdn.jsdelivr.net/jquery/3.2.1/jquery.min.js"></script>

<!-- Selectize for autocomplete example -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.default.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/js/standalone/selectize.min.js"></script>
</head>
<body>
</body>
Expand Down
72 changes: 72 additions & 0 deletions src/connectors/autocomplete/__tests__/connectAutocomplete-test.js
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');
});
});
Loading

0 comments on commit 4bec81e

Please sign in to comment.