From 78705f381cb8632c131ecbffc36153311f368e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Fri, 24 Jul 2020 14:35:31 +0200 Subject: [PATCH 1/6] feat(widgets): introduce `getWidgetRenderState` This introduces the widget lifecycle hook `getWidgetRenderState` and implements it for `connectSearchBox`. --- .../__tests__/connectSearchBox-test.js | 272 ++++++++++++------ src/connectors/search-box/connectSearchBox.js | 33 ++- src/lib/InstantSearch.ts | 2 + src/types/widget.ts | 62 +++- src/widgets/index/__tests__/index-test.ts | 203 ++++++++++++- src/widgets/index/index.ts | 140 ++++++++- .../search-box/__tests__/search-box-test.js | 21 +- test/mock/createInstantSearch.ts | 1 + test/mock/createWidget.ts | 8 + 9 files changed, 605 insertions(+), 137 deletions(-) diff --git a/src/connectors/search-box/__tests__/connectSearchBox-test.js b/src/connectors/search-box/__tests__/connectSearchBox-test.js index 08b227e530..5478652313 100644 --- a/src/connectors/search-box/__tests__/connectSearchBox-test.js +++ b/src/connectors/search-box/__tests__/connectSearchBox-test.js @@ -3,6 +3,12 @@ import algoliasearchHelper, { SearchParameters, } from 'algoliasearch-helper'; import connectSearchBox from '../connectSearchBox'; +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/mock/createWidget'; +import { InstantSearch } from '../../../types'; +import { createSearchClient } from '../../../../test/mock/createSearchClient'; describe('connectSearchBox', () => { const getInitializedWidget = (config = {}) => { @@ -16,11 +22,12 @@ describe('connectSearchBox', () => { const helper = algoliasearchHelper({}, '', initialConfig); helper.search = jest.fn(); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); const { refine } = renderFn.mock.calls[0][0]; @@ -51,7 +58,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ init: expect.any(Function), render: expect.any(Function), dispose: expect.any(Function), - + getWidgetRenderState: expect.any(Function), getWidgetUiState: expect.any(Function), getWidgetSearchParameters: expect.any(Function), }) @@ -69,11 +76,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ const helper = algoliasearchHelper({}); helper.search = () => {}; - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); expect(renderFn).toHaveBeenCalledTimes(1); expect(renderFn).toHaveBeenLastCalledWith( @@ -84,13 +92,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ true ); - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + }) + ); expect(renderFn).toHaveBeenCalledTimes(2); expect(renderFn).toHaveBeenLastCalledWith( @@ -110,11 +118,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ const helper = algoliasearchHelper({}); helper.search = jest.fn(); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); { const { refine, query } = renderFn.mock.calls[0][0]; @@ -125,13 +134,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ expect(helper.search).toHaveBeenCalledTimes(1); } - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + }) + ); { const { refine, query } = renderFn.mock.calls[1][0]; @@ -153,11 +162,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ }); helper.search = jest.fn(); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); { expect(helper.state.query).toBe('bup'); @@ -168,13 +178,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ refine('bip'); // triggers a search } - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + }) + ); { expect(helper.state.query).toBe('bip'); @@ -200,11 +210,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ const helper = algoliasearchHelper({}); helper.search = jest.fn(); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); { const { refine } = renderFn.mock.calls[0][0]; @@ -224,13 +235,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ letSearchThrough = false; - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, - }); + widget.render( + createRenderOptions({ + results: new SearchResults(helper.state, [{}]), + state: helper.state, + helper, + }) + ); { const { refine } = renderFn.mock.calls[1][0]; @@ -248,42 +259,27 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ } }); - it('should always provide the same refine() and clear() function reference', () => { - const renderFn = jest.fn(); - const makeWidget = connectSearchBox(renderFn); - const widget = makeWidget(); - - const helper = algoliasearchHelper({}); - helper.search = () => {}; - - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); - - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, - }); - - const firstRenderOptions = renderFn.mock.calls[0][0]; - - widget.render({ - results: new SearchResults(helper.state, [{}]), - state: helper.state, - helper, - createURL: () => '#', - searchMetadata: { isSearchStalled: false }, + it('provides the same `refine` and `clear` function references', done => { + const initRenderState = {}; + const createSearchBox = connectSearchBox( + ({ refine, clear }, isFirstRender) => { + if (isFirstRender) { + initRenderState.refine = refine; + initRenderState.clear = clear; + } else { + expect(refine).toBe(initRenderState.refine); + expect(clear).toBe(initRenderState.clear); + done(); + } + } + ); + const search = new InstantSearch({ + searchClient: createSearchClient(), + indexName: 'indexName', }); - const secondRenderOptions = renderFn.mock.calls[1][0]; - - expect(firstRenderOptions.clear).toBe(secondRenderOptions.clear); - expect(firstRenderOptions.refine).toBe(secondRenderOptions.refine); + search.addWidgets([createSearchBox()]); + search.start(); }); it('should clear on init as well', () => { @@ -297,11 +293,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ expect(helper.state.query).toBe('foobar'); - widget.init({ - helper, - state: helper.state, - createURL: () => '#', - }); + widget.init( + createInitOptions({ + helper, + state: helper.state, + }) + ); const { clear } = renderFn.mock.calls[0][0]; clear(); @@ -310,6 +307,95 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ expect(helper.search).toHaveBeenCalledTimes(1); }); + describe('getWidgetRenderState', () => { + test('returns the render state with default render options', () => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const queryHook = jest.fn(); + const createSearchBox = connectSearchBox(renderFn, unmountFn); + const searchBox = createSearchBox({ + queryHook, + }); + + const renderState1 = searchBox.getWidgetRenderState( + {}, + createInitOptions() + ); + + expect(renderState1.searchBox).toEqual({ + clear: expect.any(Function), + isSearchStalled: false, + query: '', + refine: undefined, + widgetParams: { queryHook }, + }); + + searchBox.init(createInitOptions()); + + const renderState2 = searchBox.getWidgetRenderState( + {}, + createRenderOptions() + ); + + expect(renderState2.searchBox).toEqual({ + clear: renderState2.searchBox.clear, + isSearchStalled: false, + query: '', + refine: expect.any(Function), + widgetParams: { + queryHook, + }, + }); + }); + + test('returns the render state with a query', () => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createSearchBox = connectSearchBox(renderFn, unmountFn); + const searchBox = createSearchBox(); + const helper = algoliasearchHelper(createSearchClient(), 'indexName', { + query: 'query', + }); + + searchBox.init(createInitOptions()); + + const renderState = searchBox.getWidgetRenderState( + {}, + createRenderOptions({ helper }) + ); + + expect(renderState.searchBox).toEqual({ + clear: expect.any(Function), + isSearchStalled: false, + query: 'query', + refine: expect.any(Function), + widgetParams: {}, + }); + }); + + test('returns the render state with stalled search', () => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createSearchBox = connectSearchBox(renderFn, unmountFn); + const searchBox = createSearchBox(); + + searchBox.init(createInitOptions()); + + const renderState = searchBox.getWidgetRenderState( + {}, + createRenderOptions({ searchMetadata: { isSearchStalled: true } }) + ); + + expect(renderState.searchBox).toEqual({ + clear: expect.any(Function), + isSearchStalled: true, + query: '', + refine: expect.any(Function), + widgetParams: {}, + }); + }); + }); + describe('dispose', () => { it('calls the unmount function', () => { const helper = algoliasearchHelper({}, ''); diff --git a/src/connectors/search-box/connectSearchBox.js b/src/connectors/search-box/connectSearchBox.js index b2a15d23bc..4b9ca1ecc9 100644 --- a/src/connectors/search-box/connectSearchBox.js +++ b/src/connectors/search-box/connectSearchBox.js @@ -72,8 +72,7 @@ export default function connectSearchBox(renderFn, unmountFn = noop) { function clear(helper) { return function() { - helper.setQuery(''); - helper.search(); + helper.setQuery('').search(); }; } @@ -86,7 +85,8 @@ export default function connectSearchBox(renderFn, unmountFn = noop) { this._clear(); }, - init({ helper, instantSearchInstance }) { + init(initOptions) { + const { helper, renderState, instantSearchInstance } = initOptions; this._cachedClear = this._cachedClear.bind(this); this._clear = clear(helper); @@ -107,27 +107,21 @@ export default function connectSearchBox(renderFn, unmountFn = noop) { renderFn( { - query: helper.state.query || '', - refine: this._refine, - clear: this._cachedClear, - widgetParams, + ...this.getWidgetRenderState(renderState, initOptions).searchBox, instantSearchInstance, }, true ); }, - render({ helper, instantSearchInstance, searchMetadata }) { + render(renderOptions) { + const { helper, renderState, instantSearchInstance } = renderOptions; this._clear = clear(helper); renderFn( { - query: helper.state.query || '', - refine: this._refine, - clear: this._cachedClear, - widgetParams, + ...this.getWidgetRenderState(renderState, renderOptions).searchBox, instantSearchInstance, - isSearchStalled: searchMetadata.isSearchStalled, }, false ); @@ -139,6 +133,19 @@ export default function connectSearchBox(renderFn, unmountFn = noop) { return state.setQueryParameter('query', undefined); }, + getWidgetRenderState(renderState, { helper, searchMetadata }) { + return { + ...renderState, + searchBox: { + query: helper.state.query || '', + refine: this._refine, + clear: this._cachedClear, + widgetParams, + isSearchStalled: searchMetadata.isSearchStalled, + }, + }; + }, + getWidgetUiState(uiState, { searchParameters }) { const query = searchParameters.query || ''; diff --git a/src/lib/InstantSearch.ts b/src/lib/InstantSearch.ts index 79e0df5448..9414030f41 100644 --- a/src/lib/InstantSearch.ts +++ b/src/lib/InstantSearch.ts @@ -17,6 +17,7 @@ import { Widget, UiState, CreateURL, + RenderState, } from '../types'; import hasDetectedInsightsClient from './utils/detect-insights-client'; import { Middleware, MiddlewareDefinition } from '../middleware'; @@ -133,6 +134,7 @@ class InstantSearch extends EventEmitter { public mainIndex: Index; public started: boolean; public templatesConfig: object; + public renderState: RenderState = {}; public _stalledSearchDelay: number; public _searchStalledTimer: any; public _isSearchStalled: boolean; diff --git a/src/types/widget.ts b/src/types/widget.ts index 75054c90a7..29f2523e1f 100644 --- a/src/types/widget.ts +++ b/src/types/widget.ts @@ -7,28 +7,19 @@ import { } from 'algoliasearch-helper'; import { InstantSearch } from './instantsearch'; -export type InitOptions = { - instantSearchInstance: InstantSearch; - parent: Index | null; - uiState: UiState; - state: SearchParameters; - helper: Helper; - templatesConfig: object; - createURL(state: SearchParameters): string; -}; - export type ScopedResult = { indexId: string; results: SearchResults; helper: Helper; }; -export type RenderOptions = { +type SharedRenderOptions = { instantSearchInstance: InstantSearch; + parent: Index | null; templatesConfig: object; - results: SearchResults; scopedResults: ScopedResult[]; state: SearchParameters; + renderState: IndexRenderState; helper: Helper; searchMetadata: { isSearchStalled: boolean; @@ -36,12 +27,21 @@ export type RenderOptions = { createURL(state: SearchParameters): string; }; +export type InitOptions = SharedRenderOptions & { + uiState: UiState; + results: undefined; +}; + +export type RenderOptions = SharedRenderOptions & { + results: SearchResults; +}; + export type DisposeOptions = { helper: Helper; state: SearchParameters; }; -export type WidgetStateOptions = { +export type WidgetUiStateOptions = { searchParameters: SearchParameters; helper: Helper; }; @@ -119,6 +119,31 @@ export type UiState = { [indexId: string]: IndexUiState; }; +export type RenderState = { + [indexId: string]: IndexRenderState; +}; + +export type IndexRenderState = Partial<{ + searchBox: WidgetRenderState< + { + query: string; + refine(query: string): void; + clear(): void; + isSearchStalled: boolean; + }, + { + queryHook?(query: string, refine: (query: string) => void); + } + >; +}>; + +type WidgetRenderState< + TWidgetRenderState, + TWidgetParams +> = TWidgetRenderState & { + widgetParams: TWidgetParams; +}; + /** * Widgets are the building blocks of InstantSearch.js. Any valid widget must * have at least a `render` or a `init` function. @@ -168,6 +193,13 @@ export type Widget = { * during this widget's initialization and life time. */ dispose?(options: DisposeOptions): SearchParameters | void; + /** + * Returns the render params to pass to the render function. + */ + getWidgetRenderState?( + renderState: IndexRenderState, + widgetRenderStateOptions: InitOptions | RenderOptions + ): IndexRenderState; /** * This function is required for a widget to be taken in account for routing. * It will derive a uiState for this widget based on the existing uiState and @@ -177,7 +209,7 @@ export type Widget = { */ getWidgetUiState?( uiState: IndexUiState, - widgetStateOptions: WidgetStateOptions + widgetUiStateOptions: WidgetUiStateOptions ): IndexUiState; /** * This function is required for a widget to be taken in account for routing. @@ -189,7 +221,7 @@ export type Widget = { */ getWidgetState?( uiState: IndexUiState, - widgetStateOptions: WidgetStateOptions + widgetStateOptions: WidgetUiStateOptions ): IndexUiState; /** * This function is required for a widget to behave correctly when a URL is diff --git a/src/widgets/index/__tests__/index-test.ts b/src/widgets/index/__tests__/index-test.ts index 3c1808519d..24cd66432a 100644 --- a/src/widgets/index/__tests__/index-test.ts +++ b/src/widgets/index/__tests__/index-test.ts @@ -11,7 +11,7 @@ import { createDisposeOptions, } from '../../../../test/mock/createWidget'; import { runAllMicroTasks } from '../../../../test/utils/runAllMicroTasks'; -import { Widget } from '../../../types'; +import { Widget, InstantSearch } from '../../../types'; import index from '../index'; import { warning } from '../../../lib/utils'; @@ -251,8 +251,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge uiState: {}, helper: instance.getHelper(), state: instance.getHelper()!.state, + renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: true, + }, }); }); }); @@ -303,8 +309,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge }, helper: instance.getHelper(), state: instance.getHelper()!.state, + renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: true, + }, }); }); @@ -1002,8 +1014,14 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge uiState: {}, helper: instance.getHelper(), state: instance.getHelper()!.state, + renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: instantSearchInstance._isSearchStalled, + }, }); }); }); @@ -1893,6 +1911,187 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge }, }); }); + + test('stores the render state on the instance', () => { + expect.assertions(2); + + const search = new InstantSearch({ + indexName: 'indexName', + searchClient: createSearchClient(), + }); + const searchIndex1 = index({ indexName: 'indexName1' }); + const searchBoxRefine = jest.fn(); + const searchBoxClear = jest.fn(); + const paginationRefine = jest.fn(); + const searchBox = createSearchBox({ + getWidgetRenderState: jest.fn( + (renderState, { helper, searchMetadata }) => { + return { + ...renderState, + searchBox: { + query: helper.state.query || '', + refine: searchBoxRefine, + clear: searchBoxClear, + isSearchStalled: searchMetadata.isSearchStalled, + widgetParams: {}, + }, + }; + } + ), + }); + const pagination = createPagination({ + getWidgetRenderState: jest.fn((renderState, { state }) => { + return { + ...renderState, + pagination: { + refine: paginationRefine, + page: state.page, + widgetParams: {}, + }, + }; + }), + }); + const renderStateWidget = { + init({ renderState }) { + expect(renderState).toEqual({ + indexName: { + searchBox: { + query: '', + refine: searchBoxRefine, + clear: searchBoxClear, + isSearchStalled: false, + widgetParams: {}, + }, + }, + indexName1: { + searchBox: { + query: '', + refine: searchBoxRefine, + clear: searchBoxClear, + isSearchStalled: false, + widgetParams: {}, + }, + pagination: { + refine: paginationRefine, + page: 0, + widgetParams: {}, + }, + }, + }); + }, + render({ renderState }) { + expect(renderState).toEqual({ + indexName: { + searchBox: { + query: '', + refine: searchBoxRefine, + clear: searchBoxClear, + isSearchStalled: false, + widgetParams: {}, + }, + }, + indexName1: { + searchBox: { + query: '', + refine: searchBoxRefine, + clear: searchBoxClear, + isSearchStalled: false, + widgetParams: {}, + }, + pagination: { + refine: paginationRefine, + page: 0, + widgetParams: {}, + }, + }, + }); + }, + }; + + search.addWidgets([ + searchBox, + searchIndex1.addWidgets([searchBox, pagination, renderStateWidget]), + ]); + search.start(); + }); + + test('calls `getWidgetRenderState` with the index render state', () => { + const searchIndex = index({ indexName: 'indexName' }); + const searchClient = createSearchClient(); + const mainHelper = algoliasearchHelper(searchClient, 'indexName', {}); + const instantSearchInstance = createInstantSearch({ mainHelper }); + const searchBox = createSearchBox({ + getWidgetRenderState: jest.fn( + (renderState, { helper, searchMetadata }) => { + return { + ...renderState, + searchBox: { + query: helper.state.query || '', + refine: () => {}, + clear: () => {}, + isSearchStalled: searchMetadata.isSearchStalled, + widgetParams: {}, + }, + }; + } + ), + }); + const pagination = createPagination({ + getWidgetRenderState: jest.fn((renderState, { state }) => { + return { + ...renderState, + pagination: { + refine: () => {}, + page: state.page, + widgetParams: {}, + }, + }; + }), + }); + + searchIndex.addWidgets([searchBox, pagination]); + + searchIndex.init( + createInitOptions({ + instantSearchInstance, + parent: null, + }) + ); + + expect(searchBox.getWidgetRenderState).toHaveBeenCalledTimes(1); + expect(searchBox.getWidgetRenderState).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + uiState: {}, + helper: expect.anything(), + state: expect.anything(), + parent: expect.anything(), + instantSearchInstance, + renderState: {}, + templatesConfig: instantSearchInstance.templatesConfig, + createURL: expect.any(Function), + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: instantSearchInstance._isSearchStalled, + }, + }) + ); + + expect(pagination.getWidgetRenderState).toHaveBeenCalledTimes(1); + expect(pagination.getWidgetRenderState).toHaveBeenCalledWith( + { + searchBox: { + clear: expect.any(Function), + isSearchStalled: true, + query: '', + refine: expect.any(Function), + widgetParams: {}, + }, + }, + expect.anything() + ); + }); }); describe('render', () => { @@ -1936,6 +2135,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge expect(widget.render).toHaveBeenCalledTimes(1); expect(widget.render).toHaveBeenCalledWith({ instantSearchInstance, + parent: instance, results: expect.any(algoliasearchHelper.SearchResults), scopedResults: [ { @@ -1945,6 +2145,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge }, ], state: expect.any(algoliasearchHelper.SearchParameters), + renderState: {}, helper: instance.getHelper(), templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index 9d8468721d..ba7f39def3 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -13,7 +13,7 @@ import { Widget, InitOptions, RenderOptions, - WidgetStateOptions, + WidgetUiStateOptions, WidgetSearchParametersOptions, ScopedResult, SearchClient, @@ -105,7 +105,7 @@ function privateHelperSetState( function getLocalWidgetsState( widgets: Widget[], - widgetStateOptions: WidgetStateOptions, + widgetStateOptions: WidgetUiStateOptions, initialUiState: IndexUiState = {} ): IndexUiState { return widgets @@ -264,16 +264,51 @@ const index = (props: IndexProps): Index => { _uiState: localUiState, }); + localWidgets.forEach(widget => { + if (widget.getWidgetRenderState) { + const widgetRenderState = widget.getWidgetRenderState( + localInstantSearchInstance!.renderState[this.getIndexId()] || {}, + { + uiState: localInstantSearchInstance!._initialUiState, + helper: this.getHelper()!, + parent: this, + instantSearchInstance: localInstantSearchInstance!, + state: helper!.state, + renderState: localInstantSearchInstance!.renderState, + templatesConfig: localInstantSearchInstance!.templatesConfig, + createURL, + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: localInstantSearchInstance!._isSearchStalled, + }, + } + ); + + storeRenderState({ + widgetRenderState, + instantSearchInstance: localInstantSearchInstance!, + parent: this, + }); + } + }); + widgets.forEach(widget => { - if (localInstantSearchInstance && widget.init) { + if (widget.init) { widget.init({ helper: helper!, parent: this, - uiState: localInstantSearchInstance._initialUiState, - instantSearchInstance: localInstantSearchInstance, + uiState: localInstantSearchInstance!._initialUiState, + instantSearchInstance: localInstantSearchInstance!, state: helper!.state, - templatesConfig: localInstantSearchInstance.templatesConfig, + renderState: localInstantSearchInstance!.renderState, + templatesConfig: localInstantSearchInstance!.templatesConfig, createURL, + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: localInstantSearchInstance!._isSearchStalled, + }, }); } }); @@ -434,6 +469,35 @@ const index = (props: IndexProps): Index => { helper!.lastResults = results; }); + localWidgets.forEach(widget => { + if (widget.getWidgetRenderState) { + const widgetRenderState = widget.getWidgetRenderState( + instantSearchInstance.renderState[this.getIndexId()] || {}, + { + uiState, + helper: this.getHelper()!, + parent: this, + instantSearchInstance, + state: helper!.state, + renderState: instantSearchInstance.renderState, + templatesConfig: instantSearchInstance.templatesConfig, + createURL, + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: instantSearchInstance._isSearchStalled, + }, + } + ); + + storeRenderState({ + widgetRenderState, + instantSearchInstance, + parent: this, + }); + } + }); + localWidgets.forEach(widget => { warning( !widget.getWidgetState, @@ -448,7 +512,13 @@ const index = (props: IndexProps): Index => { instantSearchInstance, state: helper!.state, templatesConfig: instantSearchInstance.templatesConfig, + renderState: instantSearchInstance.renderState, createURL, + results: undefined, + scopedResults: [], + searchMetadata: { + isSearchStalled: instantSearchInstance._isSearchStalled, + }, }); } }); @@ -483,6 +553,38 @@ const index = (props: IndexProps): Index => { }, render({ instantSearchInstance }: IndexRenderOptions) { + if (!this.getResults()) { + return; + } + + localWidgets.forEach(widget => { + if (widget.getWidgetRenderState) { + const widgetRenderState = widget.getWidgetRenderState( + instantSearchInstance.renderState[this.getIndexId()] || {}, + { + helper: this.getHelper()!, + parent: this, + instantSearchInstance, + results: this.getResults()!, + scopedResults: resolveScopedResultsFromIndex(this), + state: this.getResults()!._state, + renderState: instantSearchInstance.renderState, + templatesConfig: instantSearchInstance.templatesConfig, + createURL, + searchMetadata: { + isSearchStalled: instantSearchInstance._isSearchStalled, + }, + } + ); + + storeRenderState({ + widgetRenderState, + instantSearchInstance, + parent: this, + }); + } + }); + localWidgets.forEach(widget => { // At this point, all the variables used below are set. Both `helper` // and `derivedHelper` have been created at the `init` step. The attribute @@ -491,13 +593,15 @@ const index = (props: IndexProps): Index => { // be delayed. The render is triggered for the complete tree but some parts do // not have results yet. - if (widget.render && derivedHelper!.lastResults) { + if (widget.render) { widget.render({ helper: helper!, + parent: this, instantSearchInstance, - results: derivedHelper!.lastResults, + results: this.getResults()!, scopedResults: resolveScopedResultsFromIndex(this), - state: derivedHelper!.lastResults._state, + state: this.getResults()!._state, + renderState: instantSearchInstance.renderState, templatesConfig: instantSearchInstance.templatesConfig, createURL, searchMetadata: { @@ -569,3 +673,21 @@ const index = (props: IndexProps): Index => { }; export default index; + +function storeRenderState({ + widgetRenderState, + instantSearchInstance, + parent, +}) { + const parentIndexName = parent + ? parent.getIndexId() + : instantSearchInstance.mainIndex.getIndexId(); + + instantSearchInstance.renderState = { + ...instantSearchInstance.renderState, + [parentIndexName]: { + ...instantSearchInstance.renderState[parentIndexName], + ...widgetRenderState, + }, + }; +} diff --git a/src/widgets/search-box/__tests__/search-box-test.js b/src/widgets/search-box/__tests__/search-box-test.js index a1caaf740e..6c10dde519 100644 --- a/src/widgets/search-box/__tests__/search-box-test.js +++ b/src/widgets/search-box/__tests__/search-box-test.js @@ -1,5 +1,9 @@ import { render } from 'preact'; import searchBox from '../search-box'; +import { + createInitOptions, + createRenderOptions, +} from '../../../../test/mock/createWidget'; jest.mock('preact', () => { const module = require.requireActual('preact'); @@ -42,7 +46,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ test('renders during init()', () => { const widget = searchBox({ container: document.createElement('div') }); - widget.init({ helper }); + widget.init(createInitOptions({ helper })); const [firstRender] = render.mock.calls; @@ -54,8 +58,8 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ const container = document.createElement('div'); const widget = searchBox({ container }); - widget.init({ helper }); - widget.render({ helper, searchMetadata: { isSearchStalled: false } }); + widget.init(createInitOptions({ helper })); + widget.render(createRenderOptions({ helper })); const [firstRender, secondRender] = render.mock.calls; @@ -71,7 +75,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ container: document.createElement('div'), }); - widget.init({ helper }); + widget.init(createInitOptions({ helper })); const [firstRender] = render.mock.calls; @@ -81,8 +85,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/search-box/ test('sets isSearchStalled', () => { const widget = searchBox({ container: document.createElement('div') }); - widget.init({ helper }); - widget.render({ helper, searchMetadata: { isSearchStalled: true } }); + widget.init(createInitOptions({ helper })); + widget.render( + createRenderOptions({ + helper, + searchMetadata: { isSearchStalled: true }, + }) + ); const [, secondRender] = render.mock.calls; diff --git a/test/mock/createInstantSearch.ts b/test/mock/createInstantSearch.ts index 5454859e0c..4ab0536e34 100644 --- a/test/mock/createInstantSearch.ts +++ b/test/mock/createInstantSearch.ts @@ -29,6 +29,7 @@ export const createInstantSearch = ( templatesConfig: {}, insightsClient: null, middleware: [], + renderState: {}, scheduleStalledRender: defer(jest.fn()), scheduleSearch: defer(jest.fn()), scheduleRender: defer(jest.fn()), diff --git a/test/mock/createWidget.ts b/test/mock/createWidget.ts index 72aebdb5da..74bad9f7ab 100644 --- a/test/mock/createWidget.ts +++ b/test/mock/createWidget.ts @@ -20,7 +20,13 @@ export const createInitOptions = ( templatesConfig: instantSearchInstance.templatesConfig, helper: instantSearchInstance.helper!, state: instantSearchInstance.helper!.state, + renderState: instantSearchInstance.renderState, + results: undefined, + scopedResults: [], createURL: jest.fn(() => '#'), + searchMetadata: { + isSearchStalled: false, + }, ...rest, }; }; @@ -38,9 +44,11 @@ export const createRenderOptions = ( return { instantSearchInstance, + parent: instantSearchInstance.mainIndex, templatesConfig: instantSearchInstance.templatesConfig, helper, state: helper.state, + renderState: instantSearchInstance.renderState, results, scopedResults: [ { From e16eda5b85e2e29907bcf10ec240e5bfb256b08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 30 Jul 2020 10:32:09 +0200 Subject: [PATCH 2/6] fix(types): remove `results` `undefined` type --- src/types/widget.ts | 1 - src/widgets/index/__tests__/index-test.ts | 4 ---- src/widgets/index/index.ts | 4 ---- test/mock/createWidget.ts | 1 - 4 files changed, 10 deletions(-) diff --git a/src/types/widget.ts b/src/types/widget.ts index 29f2523e1f..b206c45a58 100644 --- a/src/types/widget.ts +++ b/src/types/widget.ts @@ -29,7 +29,6 @@ type SharedRenderOptions = { export type InitOptions = SharedRenderOptions & { uiState: UiState; - results: undefined; }; export type RenderOptions = SharedRenderOptions & { diff --git a/src/widgets/index/__tests__/index-test.ts b/src/widgets/index/__tests__/index-test.ts index 24cd66432a..e994c94a6b 100644 --- a/src/widgets/index/__tests__/index-test.ts +++ b/src/widgets/index/__tests__/index-test.ts @@ -254,7 +254,6 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: true, @@ -312,7 +311,6 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: true, @@ -1017,7 +1015,6 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: instantSearchInstance._isSearchStalled, @@ -2070,7 +2067,6 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge renderState: {}, templatesConfig: instantSearchInstance.templatesConfig, createURL: expect.any(Function), - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: instantSearchInstance._isSearchStalled, diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index ba7f39def3..a999a5cc82 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -277,7 +277,6 @@ const index = (props: IndexProps): Index => { renderState: localInstantSearchInstance!.renderState, templatesConfig: localInstantSearchInstance!.templatesConfig, createURL, - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: localInstantSearchInstance!._isSearchStalled, @@ -304,7 +303,6 @@ const index = (props: IndexProps): Index => { renderState: localInstantSearchInstance!.renderState, templatesConfig: localInstantSearchInstance!.templatesConfig, createURL, - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: localInstantSearchInstance!._isSearchStalled, @@ -482,7 +480,6 @@ const index = (props: IndexProps): Index => { renderState: instantSearchInstance.renderState, templatesConfig: instantSearchInstance.templatesConfig, createURL, - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: instantSearchInstance._isSearchStalled, @@ -514,7 +511,6 @@ const index = (props: IndexProps): Index => { templatesConfig: instantSearchInstance.templatesConfig, renderState: instantSearchInstance.renderState, createURL, - results: undefined, scopedResults: [], searchMetadata: { isSearchStalled: instantSearchInstance._isSearchStalled, diff --git a/test/mock/createWidget.ts b/test/mock/createWidget.ts index 74bad9f7ab..b0a58a3cf3 100644 --- a/test/mock/createWidget.ts +++ b/test/mock/createWidget.ts @@ -21,7 +21,6 @@ export const createInitOptions = ( helper: instantSearchInstance.helper!, state: instantSearchInstance.helper!.state, renderState: instantSearchInstance.renderState, - results: undefined, scopedResults: [], createURL: jest.fn(() => '#'), searchMetadata: { From 7ebd868b3544439590abf57e04a10ffbbd50ff03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 30 Jul 2020 10:32:59 +0200 Subject: [PATCH 3/6] fix(index): recompute only new added widgets render state --- src/widgets/index/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index a999a5cc82..b02bf9fe75 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -264,7 +264,7 @@ const index = (props: IndexProps): Index => { _uiState: localUiState, }); - localWidgets.forEach(widget => { + widgets.forEach(widget => { if (widget.getWidgetRenderState) { const widgetRenderState = widget.getWidgetRenderState( localInstantSearchInstance!.renderState[this.getIndexId()] || {}, From b84947b56f2238a4c5a3f131ce4c9205f94cae0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 30 Jul 2020 10:36:39 +0200 Subject: [PATCH 4/6] docs(index): explain why `getWidgetRenderState` is called in a separate loop --- src/widgets/index/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index b02bf9fe75..7902ade320 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -264,6 +264,9 @@ const index = (props: IndexProps): Index => { _uiState: localUiState, }); + // 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 => { if (widget.getWidgetRenderState) { const widgetRenderState = widget.getWidgetRenderState( @@ -467,6 +470,9 @@ const index = (props: IndexProps): Index => { helper!.lastResults = results; }); + // We compute the render state before calling `render` in a separate loop + // to construct the whole render state object that is then passed to + // `render`. localWidgets.forEach(widget => { if (widget.getWidgetRenderState) { const widgetRenderState = widget.getWidgetRenderState( From 1dde226cad5ee8c438e139bd72f10236d3614ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 30 Jul 2020 10:54:36 +0200 Subject: [PATCH 5/6] chore: increase bundlesize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09cb6ee848..f746f3da24 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "bundlesize": [ { "path": "./dist/instantsearch.production.min.js", - "maxSize": "64 kB" + "maxSize": "64.25 kB" }, { "path": "./dist/instantsearch.development.js", From a8d42cdfe3e767887fdb52172fc1adadc6ce000d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 30 Jul 2020 10:55:06 +0200 Subject: [PATCH 6/6] refactor(index): reorder params --- src/widgets/index/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index 7902ade320..888c9b80c1 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -479,7 +479,7 @@ const index = (props: IndexProps): Index => { instantSearchInstance.renderState[this.getIndexId()] || {}, { uiState, - helper: this.getHelper()!, + helper: helper!, parent: this, instantSearchInstance, state: helper!.state, @@ -514,8 +514,8 @@ const index = (props: IndexProps): Index => { parent: this, instantSearchInstance, state: helper!.state, - templatesConfig: instantSearchInstance.templatesConfig, renderState: instantSearchInstance.renderState, + templatesConfig: instantSearchInstance.templatesConfig, createURL, scopedResults: [], searchMetadata: {