Skip to content

Commit fb5ca81

Browse files
authored
feat(index): accept "composed" widgets in add/removeWidgets (#6700)
1 parent fc4e7e9 commit fb5ca81

File tree

4 files changed

+109
-89
lines changed

4 files changed

+109
-89
lines changed

packages/instantsearch.js/src/lib/InstantSearch.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ See documentation: ${createDocumentationLink({
492492
* Widgets can be added either before or after InstantSearch has started.
493493
* @param widgets The array of widgets to add to InstantSearch.
494494
*/
495-
public addWidgets(widgets: Array<Widget | IndexWidget>) {
495+
public addWidgets(widgets: Array<Widget | IndexWidget | Widget[]>) {
496496
if (!Array.isArray(widgets)) {
497497
throw new Error(
498498
withUsage(
@@ -501,23 +501,9 @@ See documentation: ${createDocumentationLink({
501501
);
502502
}
503503

504-
if (
505-
widgets.some(
506-
(widget) =>
507-
typeof widget.init !== 'function' &&
508-
typeof widget.render !== 'function'
509-
)
510-
) {
511-
throw new Error(
512-
withUsage(
513-
'The widget definition expects a `render` and/or an `init` method.'
514-
)
515-
);
516-
}
517-
518504
if (
519505
this.compositionID &&
520-
widgets.some((w) => isIndexWidget(w) && !w._isolated)
506+
widgets.some((w) => !Array.isArray(w) && isIndexWidget(w) && !w._isolated)
521507
) {
522508
throw new Error(
523509
withUsage(
@@ -553,7 +539,7 @@ See documentation: ${createDocumentationLink({
553539
*
554540
* The widgets must implement a `dispose()` method to clear their states.
555541
*/
556-
public removeWidgets(widgets: Array<Widget | IndexWidget>) {
542+
public removeWidgets(widgets: Array<Widget | IndexWidget | Widget[]>) {
557543
if (!Array.isArray(widgets)) {
558544
throw new Error(
559545
withUsage(
@@ -562,12 +548,6 @@ See documentation: ${createDocumentationLink({
562548
);
563549
}
564550

565-
if (widgets.some((widget) => typeof widget.dispose !== 'function')) {
566-
throw new Error(
567-
withUsage('The widget definition expects a `dispose` method.')
568-
);
569-
}
570-
571551
this.mainIndex.removeWidgets(widgets);
572552

573553
return this;

packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ describe('Usage', () => {
8686
// eslint-disable-next-line no-new
8787
new InstantSearch({ indexName: 'indexName', searchClient: undefined });
8888
}).toThrowErrorMatchingInlineSnapshot(`
89-
"The \`searchClient\` option is required.
89+
"The \`searchClient\` option is required.
9090
91-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
92-
`);
91+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
92+
`);
9393
});
9494

9595
it('throws if searchClient does not implement a search method', () => {
@@ -98,10 +98,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
9898
// eslint-disable-next-line no-new
9999
new InstantSearch({ indexName: 'indexName', searchClient: {} });
100100
}).toThrowErrorMatchingInlineSnapshot(`
101-
"The \`searchClient\` must implement a \`search\` method.
101+
"The \`searchClient\` must implement a \`search\` method.
102102
103-
See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/"
104-
`);
103+
See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/"
104+
`);
105105
});
106106

107107
describe('root index warning', () => {
@@ -174,10 +174,10 @@ See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend
174174
insightsClient: 'insights',
175175
});
176176
}).toThrowErrorMatchingInlineSnapshot(`
177-
"The \`insightsClient\` option should be a function.
177+
"The \`insightsClient\` option should be a function.
178178
179-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
180-
`);
179+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
180+
`);
181181
});
182182

183183
it('throws if addWidgets is called with a single widget', () => {
@@ -189,10 +189,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
189189
// @ts-expect-error
190190
search.addWidgets({});
191191
}).toThrowErrorMatchingInlineSnapshot(`
192-
"The \`addWidgets\` method expects an array of widgets. Please use \`addWidget\`.
192+
"The \`addWidgets\` method expects an array of widgets. Please use \`addWidget\`.
193193
194-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
195-
`);
194+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
195+
`);
196196
});
197197

198198
it('throws if a widget without render or init method is added', () => {
@@ -205,10 +205,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
205205
});
206206
search.addWidgets(widgets);
207207
}).toThrowErrorMatchingInlineSnapshot(`
208-
"The widget definition expects a \`render\` and/or an \`init\` method.
208+
"The widget definition expects a \`render\` and/or an \`init\` method.
209209
210-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
211-
`);
210+
See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widget/js/"
211+
`);
212212
});
213213

214214
it('does not throw with a widget having a init method', () => {
@@ -244,10 +244,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
244244
// @ts-expect-error
245245
search.removeWidgets({});
246246
}).toThrowErrorMatchingInlineSnapshot(`
247-
"The \`removeWidgets\` method expects an array of widgets. Please use \`removeWidget\`.
247+
"The \`removeWidgets\` method expects an array of widgets. Please use \`removeWidget\`.
248248
249-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
250-
`);
249+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
250+
`);
251251
});
252252

253253
it('throws if a widget without dispose method is removed', () => {
@@ -262,10 +262,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
262262
});
263263
search.removeWidgets(widgets);
264264
}).toThrowErrorMatchingInlineSnapshot(`
265-
"The widget definition expects a \`dispose\` method.
265+
"The widget definition expects a \`dispose\` method.
266266
267-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
268-
`);
267+
See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widget/js/"
268+
`);
269269
});
270270

271271
it('throws if createURL is called before start', () => {
@@ -275,10 +275,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
275275
});
276276

277277
expect(() => search.createURL()).toThrowErrorMatchingInlineSnapshot(`
278-
"The \`start\` method needs to be called before \`createURL\`.
278+
"The \`start\` method needs to be called before \`createURL\`.
279279
280-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
281-
`);
280+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
281+
`);
282282
});
283283

284284
it('throws if refresh is called before start', () => {
@@ -288,10 +288,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
288288
});
289289

290290
expect(() => search.refresh()).toThrowErrorMatchingInlineSnapshot(`
291-
"The \`start\` method needs to be called before \`refresh\`.
291+
"The \`start\` method needs to be called before \`refresh\`.
292292
293-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
294-
`);
293+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
294+
`);
295295
});
296296

297297
it('warns dev with EXPERIMENTAL_use', () => {
@@ -1241,10 +1241,10 @@ describe('start', () => {
12411241
expect(() => {
12421242
instance.start();
12431243
}).toThrowErrorMatchingInlineSnapshot(`
1244-
"The \`start\` method has already been called once.
1244+
"The \`start\` method has already been called once.
12451245
1246-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
1247-
`);
1246+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
1247+
`);
12481248
});
12491249

12501250
it('keeps a mainHelper already set on the instance (Vue SSR)', () => {
@@ -2217,10 +2217,10 @@ describe('setUiState', () => {
22172217
expect(() => {
22182218
search.setUiState({});
22192219
}).toThrowErrorMatchingInlineSnapshot(`
2220-
"The \`start\` method needs to be called before \`setUiState\`.
2220+
"The \`start\` method needs to be called before \`setUiState\`.
22212221
2222-
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
2223-
`);
2222+
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
2223+
`);
22242224
});
22252225

22262226
test('triggers a search', async () => {

packages/instantsearch.js/src/widgets/index/__tests__/index-test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,21 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
186186
expect(instance.getWidgets()).toHaveLength(2);
187187
});
188188

189+
it('accepts nested widgets', () => {
190+
const instance = index({ indexName: 'indexName' });
191+
const searchBox = virtualSearchBox({});
192+
const pagination = virtualPagination({});
193+
const refinementList = virtualRefinementList({ attribute: 'brand' });
194+
195+
instance.addWidgets([searchBox, [pagination, refinementList]]);
196+
expect(instance.getWidgets()).toHaveLength(3);
197+
expect(instance.getWidgets()).toEqual([
198+
searchBox,
199+
pagination,
200+
refinementList,
201+
]);
202+
});
203+
189204
it('returns the instance to be able to chain the calls', () => {
190205
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
191206
const subLevelInstance = index({ indexName: 'subLevelIndexName' });
@@ -499,6 +514,20 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
499514
expect(instance.getWidgets()).toHaveLength(0);
500515
});
501516

517+
it('accepts nested widgets', () => {
518+
const instance = index({ indexName: 'indexName' });
519+
const searchBox = virtualSearchBox({});
520+
const pagination = virtualPagination({});
521+
const refinementList = virtualRefinementList({ attribute: 'brand' });
522+
523+
instance.addWidgets([searchBox, pagination, refinementList]);
524+
525+
expect(instance.getWidgets()).toHaveLength(3);
526+
527+
instance.removeWidgets([searchBox, [pagination, refinementList]]);
528+
expect(instance.getWidgets()).toHaveLength(0);
529+
});
530+
502531
it('returns the instance to be able to chain the calls', () => {
503532
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
504533
const subLevelInstance = index({ indexName: 'subLevelIndexName' });

packages/instantsearch.js/src/widgets/index/index.ts

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ export type IndexWidget<TUiState extends UiState = UiState> = Omit<
126126
nextState: SearchParameters | ((state: IndexUiState) => IndexUiState)
127127
) => string;
128128

129-
addWidgets: (widgets: Array<Widget | IndexWidget>) => IndexWidget;
130-
removeWidgets: (widgets: Array<Widget | IndexWidget>) => IndexWidget;
129+
addWidgets: (widgets: Array<Widget | IndexWidget | Widget[]>) => IndexWidget;
130+
removeWidgets: (
131+
widgets: Array<Widget | IndexWidget | Widget[]>
132+
) => IndexWidget;
131133

132134
init: (options: IndexInitOptions) => void;
133135
render: (options: IndexRenderOptions) => void;
@@ -432,9 +434,13 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
432434
withUsage('The `addWidgets` method expects an array of widgets.')
433435
);
434436
}
437+
const flatWidgets = widgets.reduce<Array<Widget | IndexWidget>>(
438+
(acc, w) => acc.concat(Array.isArray(w) ? w : [w]),
439+
[]
440+
);
435441

436442
if (
437-
widgets.some(
443+
flatWidgets.some(
438444
(widget) =>
439445
typeof widget.init !== 'function' &&
440446
typeof widget.render !== 'function'
@@ -447,7 +453,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
447453
);
448454
}
449455

450-
widgets.forEach((widget) => {
456+
flatWidgets.forEach((widget) => {
451457
if (isIndexWidget(widget)) {
452458
return;
453459
}
@@ -465,8 +471,8 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
465471
addWidgetId(widget);
466472
});
467473

468-
localWidgets = localWidgets.concat(widgets);
469-
if (localInstantSearchInstance && Boolean(widgets.length)) {
474+
localWidgets = localWidgets.concat(flatWidgets);
475+
if (localInstantSearchInstance && Boolean(flatWidgets.length)) {
470476
privateHelperSetState(helper!, {
471477
state: getLocalWidgetsSearchParameters(localWidgets, {
472478
uiState: localUiState,
@@ -482,7 +488,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
482488
// We compute the render state before calling `init` in a separate loop
483489
// to construct the whole render state object that is then passed to
484490
// `init`.
485-
widgets.forEach((widget) => {
491+
flatWidgets.forEach((widget) => {
486492
if (widget.getRenderState) {
487493
const renderState = widget.getRenderState(
488494
localInstantSearchInstance!.renderState[this.getIndexId()] || {},
@@ -501,7 +507,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
501507
}
502508
});
503509

504-
widgets.forEach((widget) => {
510+
flatWidgets.forEach((widget) => {
505511
if (widget.init) {
506512
widget.init(
507513
createInitArgs(
@@ -529,15 +535,19 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
529535
withUsage('The `removeWidgets` method expects an array of widgets.')
530536
);
531537
}
538+
const flatWidgets = widgets.reduce<Array<Widget | IndexWidget>>(
539+
(acc, w) => acc.concat(Array.isArray(w) ? w : [w]),
540+
[]
541+
);
532542

533-
if (widgets.some((widget) => typeof widget.dispose !== 'function')) {
543+
if (flatWidgets.some((widget) => typeof widget.dispose !== 'function')) {
534544
throw new Error(
535545
withUsage('The widget definition expects a `dispose` method.')
536546
);
537547
}
538548

539549
localWidgets = localWidgets.filter(
540-
(widget) => widgets.indexOf(widget) === -1
550+
(widget) => flatWidgets.indexOf(widget) === -1
541551
);
542552

543553
localWidgets.forEach((widget) => {
@@ -556,30 +566,31 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
556566
}
557567
});
558568

559-
if (localInstantSearchInstance && Boolean(widgets.length)) {
560-
const { cleanedSearchState, cleanedRecommendState } = widgets.reduce(
561-
(states, widget) => {
562-
// the `dispose` method exists at this point we already assert it
563-
const next = widget.dispose!({
564-
helper: helper!,
565-
state: states.cleanedSearchState,
566-
recommendState: states.cleanedRecommendState,
567-
parent: this,
568-
});
569-
570-
if (next instanceof algoliasearchHelper.RecommendParameters) {
571-
states.cleanedRecommendState = next;
572-
} else if (next) {
573-
states.cleanedSearchState = next;
569+
if (localInstantSearchInstance && Boolean(flatWidgets.length)) {
570+
const { cleanedSearchState, cleanedRecommendState } =
571+
flatWidgets.reduce(
572+
(states, widget) => {
573+
// the `dispose` method exists at this point we already assert it
574+
const next = widget.dispose!({
575+
helper: helper!,
576+
state: states.cleanedSearchState,
577+
recommendState: states.cleanedRecommendState,
578+
parent: this,
579+
});
580+
581+
if (next instanceof algoliasearchHelper.RecommendParameters) {
582+
states.cleanedRecommendState = next;
583+
} else if (next) {
584+
states.cleanedSearchState = next;
585+
}
586+
587+
return states;
588+
},
589+
{
590+
cleanedSearchState: helper!.state,
591+
cleanedRecommendState: helper!.recommendState,
574592
}
575-
576-
return states;
577-
},
578-
{
579-
cleanedSearchState: helper!.state,
580-
cleanedRecommendState: helper!.recommendState,
581-
}
582-
);
593+
);
583594

584595
const newState = localInstantSearchInstance.future
585596
.preserveSharedStateOnUnmount

0 commit comments

Comments
 (0)