Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions packages/instantsearch.js/src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ See documentation: ${createDocumentationLink({
* Widgets can be added either before or after InstantSearch has started.
* @param widgets The array of widgets to add to InstantSearch.
*/
public addWidgets(widgets: Array<Widget | IndexWidget>) {
public addWidgets(widgets: Array<Widget | IndexWidget | Widget[]>) {
if (!Array.isArray(widgets)) {
throw new Error(
withUsage(
Expand All @@ -501,23 +501,9 @@ See documentation: ${createDocumentationLink({
);
}

if (
widgets.some(
(widget) =>
typeof widget.init !== 'function' &&
typeof widget.render !== 'function'
)
) {
throw new Error(
withUsage(
'The widget definition expects a `render` and/or an `init` method.'
)
);
}

if (
this.compositionID &&
widgets.some((w) => isIndexWidget(w) && !w._isolated)
widgets.some((w) => !Array.isArray(w) && isIndexWidget(w) && !w._isolated)
) {
throw new Error(
withUsage(
Expand Down Expand Up @@ -553,7 +539,7 @@ See documentation: ${createDocumentationLink({
*
* The widgets must implement a `dispose()` method to clear their states.
*/
public removeWidgets(widgets: Array<Widget | IndexWidget>) {
public removeWidgets(widgets: Array<Widget | IndexWidget | Widget[]>) {
if (!Array.isArray(widgets)) {
throw new Error(
withUsage(
Expand All @@ -562,12 +548,6 @@ See documentation: ${createDocumentationLink({
);
}

if (widgets.some((widget) => typeof widget.dispose !== 'function')) {
throw new Error(
withUsage('The widget definition expects a `dispose` method.')
);
}

this.mainIndex.removeWidgets(widgets);

return this;
Expand Down
66 changes: 33 additions & 33 deletions packages/instantsearch.js/src/lib/__tests__/InstantSearch-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ describe('Usage', () => {
// eslint-disable-next-line no-new
new InstantSearch({ indexName: 'indexName', searchClient: undefined });
}).toThrowErrorMatchingInlineSnapshot(`
"The \`searchClient\` option is required.
"The \`searchClient\` option is required.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('throws if searchClient does not implement a search method', () => {
Expand All @@ -98,10 +98,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
// eslint-disable-next-line no-new
new InstantSearch({ indexName: 'indexName', searchClient: {} });
}).toThrowErrorMatchingInlineSnapshot(`
"The \`searchClient\` must implement a \`search\` method.
"The \`searchClient\` must implement a \`search\` method.

See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/"
`);
See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js/"
`);
});

describe('root index warning', () => {
Expand Down Expand Up @@ -174,10 +174,10 @@ See: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend
insightsClient: 'insights',
});
}).toThrowErrorMatchingInlineSnapshot(`
"The \`insightsClient\` option should be a function.
"The \`insightsClient\` option should be a function.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('throws if addWidgets is called with a single widget', () => {
Expand All @@ -189,10 +189,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
// @ts-expect-error
search.addWidgets({});
}).toThrowErrorMatchingInlineSnapshot(`
"The \`addWidgets\` method expects an array of widgets. Please use \`addWidget\`.
"The \`addWidgets\` method expects an array of widgets. Please use \`addWidget\`.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('throws if a widget without render or init method is added', () => {
Expand All @@ -205,10 +205,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
});
search.addWidgets(widgets);
}).toThrowErrorMatchingInlineSnapshot(`
"The widget definition expects a \`render\` and/or an \`init\` method.
"The widget definition expects a \`render\` and/or an \`init\` method.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widget/js/"
`);
});

it('does not throw with a widget having a init method', () => {
Expand Down Expand Up @@ -244,10 +244,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
// @ts-expect-error
search.removeWidgets({});
}).toThrowErrorMatchingInlineSnapshot(`
"The \`removeWidgets\` method expects an array of widgets. Please use \`removeWidget\`.
"The \`removeWidgets\` method expects an array of widgets. Please use \`removeWidget\`.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('throws if a widget without dispose method is removed', () => {
Expand All @@ -262,10 +262,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsear
});
search.removeWidgets(widgets);
}).toThrowErrorMatchingInlineSnapshot(`
"The widget definition expects a \`dispose\` method.
"The widget definition expects a \`dispose\` method.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widget/js/"
`);
});

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

expect(() => search.createURL()).toThrowErrorMatchingInlineSnapshot(`
"The \`start\` method needs to be called before \`createURL\`.
"The \`start\` method needs to be called before \`createURL\`.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

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

expect(() => search.refresh()).toThrowErrorMatchingInlineSnapshot(`
"The \`start\` method needs to be called before \`refresh\`.
"The \`start\` method needs to be called before \`refresh\`.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('warns dev with EXPERIMENTAL_use', () => {
Expand Down Expand Up @@ -1241,10 +1241,10 @@ describe('start', () => {
expect(() => {
instance.start();
}).toThrowErrorMatchingInlineSnapshot(`
"The \`start\` method has already been called once.
"The \`start\` method has already been called once.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

it('keeps a mainHelper already set on the instance (Vue SSR)', () => {
Expand Down Expand Up @@ -2217,10 +2217,10 @@ describe('setUiState', () => {
expect(() => {
search.setUiState({});
}).toThrowErrorMatchingInlineSnapshot(`
"The \`start\` method needs to be called before \`setUiState\`.
"The \`start\` method needs to be called before \`setUiState\`.

See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
See documentation: https://www.algolia.com/doc/api-reference/widgets/instantsearch/js/"
`);
});

test('triggers a search', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,21 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
expect(instance.getWidgets()).toHaveLength(2);
});

it('accepts nested widgets', () => {
const instance = index({ indexName: 'indexName' });
const searchBox = virtualSearchBox({});
const pagination = virtualPagination({});
const refinementList = virtualRefinementList({ attribute: 'brand' });

instance.addWidgets([searchBox, [pagination, refinementList]]);
expect(instance.getWidgets()).toHaveLength(3);
expect(instance.getWidgets()).toEqual([
searchBox,
pagination,
refinementList,
]);
});

it('returns the instance to be able to chain the calls', () => {
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });
Expand Down Expand Up @@ -499,6 +514,20 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
expect(instance.getWidgets()).toHaveLength(0);
});

it('accepts nested widgets', () => {
const instance = index({ indexName: 'indexName' });
const searchBox = virtualSearchBox({});
const pagination = virtualPagination({});
const refinementList = virtualRefinementList({ attribute: 'brand' });

instance.addWidgets([searchBox, pagination, refinementList]);

expect(instance.getWidgets()).toHaveLength(3);

instance.removeWidgets([searchBox, [pagination, refinementList]]);
expect(instance.getWidgets()).toHaveLength(0);
});

it('returns the instance to be able to chain the calls', () => {
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });
Expand Down
77 changes: 44 additions & 33 deletions packages/instantsearch.js/src/widgets/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ export type IndexWidget<TUiState extends UiState = UiState> = Omit<
nextState: SearchParameters | ((state: IndexUiState) => IndexUiState)
) => string;

addWidgets: (widgets: Array<Widget | IndexWidget>) => IndexWidget;
removeWidgets: (widgets: Array<Widget | IndexWidget>) => IndexWidget;
addWidgets: (widgets: Array<Widget | IndexWidget | Widget[]>) => IndexWidget;
removeWidgets: (
widgets: Array<Widget | IndexWidget | Widget[]>
) => IndexWidget;

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

if (
widgets.some(
flatWidgets.some(
(widget) =>
typeof widget.init !== 'function' &&
typeof widget.render !== 'function'
Expand All @@ -447,7 +453,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
);
}

widgets.forEach((widget) => {
flatWidgets.forEach((widget) => {
if (isIndexWidget(widget)) {
return;
}
Expand All @@ -465,8 +471,8 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
addWidgetId(widget);
});

localWidgets = localWidgets.concat(widgets);
if (localInstantSearchInstance && Boolean(widgets.length)) {
localWidgets = localWidgets.concat(flatWidgets);
if (localInstantSearchInstance && Boolean(flatWidgets.length)) {
privateHelperSetState(helper!, {
state: getLocalWidgetsSearchParameters(localWidgets, {
uiState: localUiState,
Expand All @@ -482,7 +488,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
// We compute the render state before calling `init` in a separate loop
// to construct the whole render state object that is then passed to
// `init`.
widgets.forEach((widget) => {
flatWidgets.forEach((widget) => {
if (widget.getRenderState) {
const renderState = widget.getRenderState(
localInstantSearchInstance!.renderState[this.getIndexId()] || {},
Expand All @@ -501,7 +507,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
}
});

widgets.forEach((widget) => {
flatWidgets.forEach((widget) => {
if (widget.init) {
widget.init(
createInitArgs(
Expand Down Expand Up @@ -529,15 +535,19 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
withUsage('The `removeWidgets` method expects an array of widgets.')
);
}
const flatWidgets = widgets.reduce<Array<Widget | IndexWidget>>(
(acc, w) => acc.concat(Array.isArray(w) ? w : [w]),
[]
);

if (widgets.some((widget) => typeof widget.dispose !== 'function')) {
if (flatWidgets.some((widget) => typeof widget.dispose !== 'function')) {
throw new Error(
withUsage('The widget definition expects a `dispose` method.')
);
}

localWidgets = localWidgets.filter(
(widget) => widgets.indexOf(widget) === -1
(widget) => flatWidgets.indexOf(widget) === -1
);

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

if (localInstantSearchInstance && Boolean(widgets.length)) {
const { cleanedSearchState, cleanedRecommendState } = widgets.reduce(
(states, widget) => {
// the `dispose` method exists at this point we already assert it
const next = widget.dispose!({
helper: helper!,
state: states.cleanedSearchState,
recommendState: states.cleanedRecommendState,
parent: this,
});

if (next instanceof algoliasearchHelper.RecommendParameters) {
states.cleanedRecommendState = next;
} else if (next) {
states.cleanedSearchState = next;
if (localInstantSearchInstance && Boolean(flatWidgets.length)) {
const { cleanedSearchState, cleanedRecommendState } =
flatWidgets.reduce(
(states, widget) => {
// the `dispose` method exists at this point we already assert it
const next = widget.dispose!({
helper: helper!,
state: states.cleanedSearchState,
recommendState: states.cleanedRecommendState,
parent: this,
});

if (next instanceof algoliasearchHelper.RecommendParameters) {
states.cleanedRecommendState = next;
} else if (next) {
states.cleanedSearchState = next;
}

return states;
},
{
cleanedSearchState: helper!.state,
cleanedRecommendState: helper!.recommendState,
}

return states;
},
{
cleanedSearchState: helper!.state,
cleanedRecommendState: helper!.recommendState,
}
);
);

const newState = localInstantSearchInstance.future
.preserveSharedStateOnUnmount
Expand Down