From 131b1ce27d03c127204615a015fd5d45c05f6a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Mon, 22 Jul 2019 14:53:52 +0200 Subject: [PATCH] feat(index): reset page of child indexes (#3962) --- src/widgets/index/__tests__/index-test.ts | 405 ++++++++++++++++++++++ src/widgets/index/index.ts | 29 ++ stories/index.stories.ts | 70 ++++ 3 files changed, 504 insertions(+) diff --git a/src/widgets/index/__tests__/index-test.ts b/src/widgets/index/__tests__/index-test.ts index 21f2379826..576728597b 100644 --- a/src/widgets/index/__tests__/index-test.ts +++ b/src/widgets/index/__tests__/index-test.ts @@ -809,6 +809,411 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/" }); }); }); + + it('resets pages of nested indexes when the state changes', () => { + const level0 = index({ indexName: 'level_0_index_name' }); + const level1 = index({ indexName: 'level_1_index_name' }); + const level2 = index({ indexName: 'level_2_index_name' }); + const level3 = index({ indexName: 'level_3_index_name' }); + const searchClient = createSearchClient(); + const mainHelper = algoliasearchHelper(searchClient, '', {}); + const instantSearchInstance = createInstantSearch({ + mainHelper, + }); + + level0.addWidgets([ + createWidget({ + getConfiguration() { + return { + hitsPerPage: 5, + }; + }, + }), + + createSearchBox({ + getConfiguration() { + return { + query: 'Apple', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 1, + }; + }, + }), + + level1.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 2, + }; + }, + }), + + level2.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 3, + }; + }, + }), + + level3.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS Red', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 4, + }; + }, + }), + ]), + ]), + ]), + ]); + + level0.init( + createInitOptions({ + instantSearchInstance, + }) + ); + + // Setting a query is considered as a change + level1 + .getHelper()! + .setQuery('Hey') + .search(); + + expect(searchClient.search).toHaveBeenCalledWith( + expect.arrayContaining([ + { + indexName: 'level_0_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple', + page: 1, + }), + }, + { + indexName: 'level_1_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Hey', + page: 0, + }), + }, + { + indexName: 'level_2_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS', + page: 0, + }), + }, + { + indexName: 'level_3_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS Red', + page: 0, + }), + }, + ]) + ); + }); + + it('does not reset pages of nested indexes when only the page changes', () => { + const level0 = index({ indexName: 'level_0_index_name' }); + const level1 = index({ indexName: 'level_1_index_name' }); + const level2 = index({ indexName: 'level_2_index_name' }); + const level3 = index({ indexName: 'level_3_index_name' }); + const searchClient = createSearchClient(); + const mainHelper = algoliasearchHelper(searchClient, '', {}); + const instantSearchInstance = createInstantSearch({ + mainHelper, + }); + + level0.addWidgets([ + createWidget({ + getConfiguration() { + return { + hitsPerPage: 5, + }; + }, + }), + + createSearchBox({ + getConfiguration() { + return { + query: 'Apple', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 1, + }; + }, + }), + + level1.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 2, + }; + }, + }), + + level2.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 3, + }; + }, + }), + + level3.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS Red', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 4, + }; + }, + }), + ]), + ]), + ]), + ]); + + level0.init( + createInitOptions({ + instantSearchInstance, + }) + ); + + level1 + .getHelper()! + .setPage(4) + .search(); + + expect(searchClient.search).toHaveBeenCalledWith( + expect.arrayContaining([ + { + indexName: 'level_0_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple', + page: 1, + }), + }, + { + indexName: 'level_1_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone', + page: 4, + }), + }, + { + indexName: 'level_2_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS', + page: 3, + }), + }, + { + indexName: 'level_3_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS Red', + page: 4, + }), + }, + ]) + ); + }); + + it('is a noop for unset pages of nested indexes when the state changes', () => { + const level0 = index({ indexName: 'level_0_index_name' }); + const level1 = index({ indexName: 'level_1_index_name' }); + const level2 = index({ indexName: 'level_2_index_name' }); + const level3 = index({ indexName: 'level_3_index_name' }); + const searchClient = createSearchClient(); + const mainHelper = algoliasearchHelper(searchClient, '', {}); + const instantSearchInstance = createInstantSearch({ + mainHelper, + }); + + level0.addWidgets([ + createWidget({ + getConfiguration() { + return { + hitsPerPage: 5, + }; + }, + }), + + createSearchBox({ + getConfiguration() { + return { + query: 'Apple', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 1, + }; + }, + }), + + level1.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone', + }; + }, + }), + + createPagination({ + getConfiguration() { + return { + page: 2, + }; + }, + }), + + level2.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS', + }; + }, + }), + + level3.addWidgets([ + createSearchBox({ + getConfiguration() { + return { + query: 'Apple iPhone XS Red', + }; + }, + }), + ]), + ]), + ]), + ]); + + level0.init( + createInitOptions({ + instantSearchInstance, + }) + ); + + level1 + .getHelper()! + .setQuery('Hey') + .search(); + + expect(searchClient.search).toHaveBeenCalledWith( + expect.arrayContaining([ + { + indexName: 'level_0_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple', + page: 1, + }), + }, + { + indexName: 'level_1_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Hey', + page: 0, + }), + }, + { + indexName: 'level_2_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS', + page: 0, + }), + }, + { + indexName: 'level_3_index_name', + params: expect.objectContaining({ + hitsPerPage: 5, + query: 'Apple iPhone XS Red', + page: 0, + }), + }, + ]) + ); + }); }); describe('render', () => { diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index d3fb50b8d2..1a6a139d96 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -37,6 +37,29 @@ export type Index = Widget & { dispose(options: DisposeOptions): void; }; +function isIndexWidget(widget: Widget): widget is Index { + return widget.$$type === 'ais.index'; +} + +function resetPageFromWidgets(widgets: Widget[]): void { + const indexWidgets = widgets.filter(isIndexWidget); + + if (indexWidgets.length === 0) { + return; + } + + indexWidgets.forEach(widget => { + const widgetHelper = widget.getHelper()!; + + widgetHelper.overrideStateWithoutTriggeringChangeEvent( + // @ts-ignore @TODO: remove "ts-ignore" once `resetPage()` is typed in the helper + widgetHelper.state.resetPage() + ); + + resetPageFromWidgets(widget.getWidgets()); + }); +} + const index = (props: IndexProps): Index => { const { indexName = null } = props || {}; @@ -208,6 +231,12 @@ const index = (props: IndexProps): Index => { mergeSearchParameters(...resolveSearchParameters(this)) ); + helper.on('change', ({ isPageReset }) => { + if (isPageReset) { + resetPageFromWidgets(localWidgets); + } + }); + derivedHelper.on('search', () => { // The index does not manage the "staleness" of the search. This is the // responsibility of the main instance. It does not make sense to manage diff --git a/stories/index.stories.ts b/stories/index.stories.ts index 31b94bd797..f23a6c6f1b 100644 --- a/stories/index.stories.ts +++ b/stories/index.stories.ts @@ -63,6 +63,76 @@ storiesOf('Index', module) ]); }) ) + .add( + 'with nested levels', + withHits(({ search, container, instantsearch }) => { + const instantSearchPriceAscTitle = document.createElement('h3'); + instantSearchPriceAscTitle.innerHTML = + 'instant_search_price_asc'; + const instantSearchPriceAscHits = document.createElement('div'); + const instantSearchPriceAsc = document.createElement('div'); + const instantSearchPriceAscPagination = document.createElement('div'); + + container.appendChild(instantSearchPriceAscTitle); + container.appendChild(instantSearchPriceAsc); + container.appendChild(instantSearchPriceAscHits); + container.appendChild(instantSearchPriceAscPagination); + + const instantSearchRatingAscTitle = document.createElement('h3'); + instantSearchRatingAscTitle.innerHTML = + 'instant_search_price_asc > instant_search_rating_asc'; + const instantSearchRatingAsc = document.createElement('div'); + const instantSearchRatingAscHits = document.createElement('div'); + const instantSearchRatingAscPagination = document.createElement('div'); + + container.appendChild(instantSearchRatingAscTitle); + container.appendChild(instantSearchRatingAsc); + container.appendChild(instantSearchRatingAscHits); + container.appendChild(instantSearchRatingAscPagination); + + search.addWidgets([ + instantsearch.widgets + .index({ indexName: 'instant_search_price_asc' }) + .addWidgets([ + instantsearch.widgets.configure({ + hitsPerPage: 2, + }), + instantsearch.widgets.hits({ + container: instantSearchPriceAscHits, + templates: { + item: hitsItemTemplate, + }, + cssClasses: { + item: 'hits-item', + }, + }), + instantsearch.widgets.pagination({ + container: instantSearchPriceAscPagination, + }), + + instantsearch.widgets + .index({ indexName: 'instant_search_rating_asc' }) + .addWidgets([ + instantsearch.widgets.configure({ + hitsPerPage: 1, + }), + instantsearch.widgets.hits({ + container: instantSearchRatingAscHits, + templates: { + item: hitsItemTemplate, + }, + cssClasses: { + item: 'hits-item', + }, + }), + instantsearch.widgets.pagination({ + container: instantSearchRatingAscPagination, + }), + ]), + ]), + ]); + }) + ) .add( 'with add/remove', withHits(({ search, container, instantsearch }) => {