Skip to content

Commit 4bec81e

Browse files
Maxime Jantonbobylito
authored andcommitted
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
1 parent d1f99fb commit 4bec81e

File tree

6 files changed

+455
-0
lines changed

6 files changed

+455
-0
lines changed

dev/app/jquery/init-stories.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import initSortBySelectorStories from './stories/sort-by-selector.stories';
1515
import initStarRatingStories from './stories/star-rating.stories';
1616
import initStatsStories from './stories/stats.stories';
1717
import initToggleStories from './stories/toggle.stories';
18+
import initAutcompleteStories from './stories/autocomplete.stories';
1819

1920
export default () => {
2021
initClearAllStories();
@@ -34,4 +35,5 @@ export default () => {
3435
initStarRatingStories();
3536
initStatsStories();
3637
initToggleStories();
38+
initAutcompleteStories();
3739
};
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/* eslint-disable import/default */
2+
3+
import { action, storiesOf } from 'dev-novel';
4+
5+
import instantsearch from '../../../../index.js';
6+
import { wrapWithHitsAndJquery } from '../../utils/wrap-with-hits.js';
7+
8+
const stories = storiesOf('Autocomplete');
9+
10+
// Widget to search into brands, select one and set it as query
11+
const autocompleteBrands = instantsearch.connectors.connectAutocomplete(
12+
({ indices, refine, widgetParams: { containerNode } }, isFirstRendering) => {
13+
if (isFirstRendering) {
14+
containerNode.html(`
15+
<strong>Search for a brand:</strong>
16+
<select id="ais-autocomplete"></select>
17+
`);
18+
19+
containerNode.find('select').selectize({
20+
options: [],
21+
22+
valueField: 'brand',
23+
labelField: 'brand',
24+
searchField: 'brand',
25+
26+
highlight: false,
27+
28+
onType: refine,
29+
30+
onChange: refine,
31+
});
32+
}
33+
34+
if (!isFirstRendering && indices[0].results) {
35+
const autocompleteInstance = containerNode.find('select')[0].selectize;
36+
37+
indices[0].results.hits.forEach(h => autocompleteInstance.addOption(h));
38+
autocompleteInstance.refreshOptions(autocompleteInstance.isOpen);
39+
}
40+
}
41+
);
42+
43+
// widget to search into hits, select a choice open a new page (event example)
44+
const autocompleteAndSelect = instantsearch.connectors.connectAutocomplete(
45+
({ indices, refine, widgetParams: { containerNode } }, isFirstRendering) => {
46+
const onItemSelected = objectID => {
47+
const item = indices.reduce((match, index) => {
48+
if (match) return match;
49+
return index.hits.find(obj => obj.objectID === objectID);
50+
}, null);
51+
52+
action('item:selected')(item);
53+
};
54+
55+
if (isFirstRendering) {
56+
containerNode.html(`
57+
<strong>Search for anything:</strong>
58+
<select id="ais-autocomplete"></select>
59+
`);
60+
61+
containerNode.find('select').selectize({
62+
options: [],
63+
64+
valueField: 'objectID',
65+
labelField: 'name',
66+
searchField: ['name', 'brand', 'categories', 'description'],
67+
68+
render: {
69+
option: item => `
70+
<div class="hit">
71+
<div class="hit-picture">
72+
<img src="${item.image}" />
73+
</div>
74+
75+
<div class="hit-content">
76+
<div>
77+
<span>${item._highlightResult.name.value}</span>
78+
<span>${item.price_formatted}</span>
79+
<span>${item.rating} stars</span>
80+
</div>
81+
82+
<div class="hit-type">
83+
${item._highlightResult.type.value}
84+
</div>
85+
86+
<div class="hit-description">
87+
${item._highlightResult.description.value}
88+
</div>
89+
</div>
90+
</div>
91+
`,
92+
},
93+
94+
highlight: false,
95+
onType: refine,
96+
97+
onChange: onItemSelected,
98+
});
99+
100+
// HACK: bind `autocompleteInstance.search` with an empty query so it returns
101+
// all the hits sent by Algolia
102+
const autocompleteInstance = containerNode.find('select')[0].selectize;
103+
autocompleteInstance.search.bind(autocompleteInstance, '');
104+
}
105+
106+
if (!isFirstRendering && indices[0].results) {
107+
const autocompleteInstance = containerNode.find('select')[0].selectize;
108+
109+
// first clear options
110+
autocompleteInstance.clearOptions();
111+
// add new ones
112+
indices[0].results.hits.forEach(h => autocompleteInstance.addOption(h));
113+
// refresh the view
114+
autocompleteInstance.refreshOptions(autocompleteInstance.isOpen);
115+
}
116+
}
117+
);
118+
119+
const multiIndex = instantsearch.connectors.connectAutocomplete(
120+
(
121+
{ indices, currentRefinement, widgetParams: { containerNode } },
122+
isFirstRendering
123+
) => {
124+
if (isFirstRendering) {
125+
containerNode.append(`
126+
<div style="width: 100%">
127+
<div
128+
id="hits0"
129+
style="width: 45%; margin-right: 5%; float: left;"
130+
>
131+
</div>
132+
133+
<div
134+
id="hits1"
135+
style="width: 50%; float: right"
136+
>
137+
</div>
138+
139+
<div style="clear: both;"></div>
140+
</div>
141+
`);
142+
}
143+
144+
// display hits
145+
indices.forEach(({ hits }, index) => {
146+
const hitsHTML =
147+
hits.length === 0
148+
? `No results for query <em>${currentRefinement}</em>`
149+
: hits.map(
150+
hit => `
151+
<div class="hit">
152+
<div class="hit-picture">
153+
<img src="${hit.image}" />
154+
</div>
155+
156+
<div class="hit-content">
157+
<div>
158+
<span>${hit._highlightResult.name.value}</span>
159+
</div>
160+
161+
<div class="hit-type">
162+
${hit._highlightResult.type.value}
163+
</div>
164+
</div>
165+
</div>
166+
`
167+
);
168+
169+
containerNode.find(`#hits${index}`).html(hitsHTML);
170+
});
171+
}
172+
);
173+
174+
export default () => {
175+
stories
176+
.add(
177+
'default',
178+
wrapWithHitsAndJquery(containerNode => {
179+
window.search.addWidget(autocompleteBrands({ containerNode }));
180+
})
181+
)
182+
.add(
183+
'Autcomplete into hits',
184+
wrapWithHitsAndJquery(containerNode =>
185+
window.search.addWidget(autocompleteAndSelect({ containerNode }))
186+
)
187+
)
188+
.add(
189+
'Multi index',
190+
wrapWithHitsAndJquery(containerNode => {
191+
containerNode.append('<div id="multi-index-search-box"></div>');
192+
window.search.addWidget(
193+
instantsearch.widgets.searchBox({
194+
container: '#multi-index-search-box',
195+
placeholder: 'Search into the two indices',
196+
poweredBy: false,
197+
autofocus: false,
198+
})
199+
);
200+
window.search.addWidget(
201+
multiIndex({
202+
containerNode,
203+
indices: [{ label: 'ikea', value: 'ikea' }],
204+
})
205+
);
206+
})
207+
);
208+
};

dev/template.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
66
<title>Instant search demo built with instantsearch.js</title>
77
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=default,Array.from"></script>
8+
9+
<!-- jQuery -->
810
<script src="https://cdn.jsdelivr.net/jquery/3.2.1/jquery.min.js"></script>
11+
12+
<!-- Selectize for autocomplete example -->
13+
<link href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.min.css" rel="stylesheet" />
14+
<link href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.default.min.css" rel="stylesheet" />
15+
<script src="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/js/standalone/selectize.min.js"></script>
916
</head>
1017
<body>
1118
</body>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import jsHelper from 'algoliasearch-helper';
2+
import connectAutocomplete from '../connectAutocomplete.js';
3+
4+
const fakeClient = { addAlgoliaAgent: () => {} };
5+
6+
describe('connectAutocomplete', () => {
7+
it('throws without `renderFn`', () => {
8+
expect(() => connectAutocomplete()).toThrow();
9+
});
10+
11+
it('renders during init and render', () => {
12+
const renderFn = jest.fn();
13+
const makeWidget = connectAutocomplete(renderFn);
14+
const widget = makeWidget();
15+
16+
expect(renderFn).toHaveBeenCalledTimes(0);
17+
18+
const helper = jsHelper(fakeClient, '', {});
19+
helper.search = jest.fn();
20+
21+
widget.init({
22+
helper,
23+
instantSearchInstance: {},
24+
});
25+
26+
expect(renderFn).toHaveBeenCalledTimes(1);
27+
expect(renderFn.mock.calls[0][1]).toBeTruthy();
28+
29+
widget.render({
30+
widgetParams: {},
31+
indices: widget.indices,
32+
instantSearchInstance: widget.instantSearchInstance,
33+
});
34+
35+
expect(renderFn).toHaveBeenCalledTimes(2);
36+
expect(renderFn.mock.calls[1][1]).toBeFalsy();
37+
});
38+
39+
it('creates derived helper', () => {
40+
const renderFn = jest.fn();
41+
const makeWidget = connectAutocomplete(renderFn);
42+
const widget = makeWidget({ indices: [{ label: 'foo', value: 'foo' }] });
43+
44+
const helper = jsHelper(fakeClient, '', {});
45+
helper.search = jest.fn();
46+
47+
widget.init({ helper, instantSearchInstance: {} });
48+
expect(renderFn).toHaveBeenCalledTimes(1);
49+
50+
// original helper + derived one
51+
const renderOpts = renderFn.mock.calls[0][0];
52+
expect(renderOpts.indices).toHaveLength(2);
53+
});
54+
55+
it('set a query and trigger search on `refine`', () => {
56+
const renderFn = jest.fn();
57+
const makeWidget = connectAutocomplete(renderFn);
58+
const widget = makeWidget();
59+
60+
const helper = jsHelper(fakeClient, '', {});
61+
helper.search = jest.fn();
62+
63+
widget.init({ helper, instantSearchInstance: {} });
64+
65+
const { refine } = renderFn.mock.calls[0][0];
66+
refine('foo');
67+
68+
expect(refine).toBe(widget._refine);
69+
expect(helper.search).toHaveBeenCalledTimes(1);
70+
expect(helper.getState().query).toBe('foo');
71+
});
72+
});

0 commit comments

Comments
 (0)