From 5f8ba5ddcf5e32fd3cecf39ea667d8266dab35f8 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Wed, 4 Aug 2021 15:20:50 +0200 Subject: [PATCH] feat(ts): allow custom ui state and route state in routing (#4816) * feat(ts): allow custom ui state and route state in routing While I couldn't find a way to make InstantSearch itself generic (this gets passed to many places, which then loses generic), using the routing middleware directly is possible like this now ```ts import instantsearch from 'instantsearch.js/es' import { history } from 'instantsearch.js/es/lib/routers'; import { createRouterMiddleware } from 'instantsearch.js/es/middlewares'; import { StateMapping, UiState } from 'instantsearch.js/es/types'; type SwagIndexUiState = { swag: boolean }; type SwagUiState = { [indexName: string]: SwagIndexUiState }; const stateMapping: StateMapping = { stateToRoute(uiState) { return Object.keys(uiState).reduce( (state, indexId) => ({ ...state, [indexId]: { swag: uiState[indexId].swag }, }), {} ); }, routeToState(routeState = {}) { return Object.keys(routeState).reduce( (state, indexId) => ({ ...state, [indexId]: routeState[indexId], }), {} ); }, }; const search = instantsearch(); search.use( createRouterMiddleware({ router: history(), stateMapping, }) ); search.addWidgets([instantsearch.widgets.hits({ container })]); ``` * actually genericificate InstantSearch * already inferred so no need * Apply suggestions from code review * address feedback * better comment --- src/index.es.ts | 10 +- src/lib/InstantSearch.ts | 14 +- src/lib/__tests__/RoutingManager-test.ts | 44 +++--- src/lib/main.ts | 10 +- src/lib/routers/__tests__/history.test.ts | 2 +- src/lib/routers/history.ts | 133 +++++++++--------- .../stateMappings/__tests__/simple-test.ts | 5 +- .../__tests__/singleIndex-test.ts | 6 +- src/lib/stateMappings/simple.ts | 20 +-- src/lib/stateMappings/singleIndex.ts | 22 +-- src/middlewares/createRouterMiddleware.ts | 47 +++++-- src/types/middleware.ts | 8 +- src/types/router.ts | 21 ++- src/widgets/index/index.ts | 6 +- 14 files changed, 199 insertions(+), 149 deletions(-) diff --git a/src/index.es.ts b/src/index.es.ts index a526fcab2c..dcc4671bcc 100644 --- a/src/index.es.ts +++ b/src/index.es.ts @@ -1,4 +1,4 @@ -import { InstantSearchOptions } from './types'; +import { InstantSearchOptions, UiState } from './types'; import InstantSearch from './lib/InstantSearch'; import version from './lib/version'; import { @@ -11,8 +11,12 @@ import { } from './helpers'; import { createInfiniteHitsSessionStorageCache } from './lib/infiniteHitsCache'; -const instantsearch = (options: InstantSearchOptions): InstantSearch => - new InstantSearch(options); +const instantsearch = < + TUiState = Record, + TRouteState = TUiState +>( + options: InstantSearchOptions +) => new InstantSearch(options); instantsearch.version = version; instantsearch.snippet = snippet; diff --git a/src/lib/InstantSearch.ts b/src/lib/InstantSearch.ts index b16facc38d..e70bd28549 100644 --- a/src/lib/InstantSearch.ts +++ b/src/lib/InstantSearch.ts @@ -42,7 +42,10 @@ function defaultCreateURL() { /** * Global options for an InstantSearch instance. */ -export type InstantSearchOptions = { +export type InstantSearchOptions< + TUiState extends UiState = UiState, + TRouteState = TUiState +> = { /** * The name of the main index */ @@ -120,7 +123,7 @@ export type InstantSearchOptions = { * Router configuration used to save the UI State into the URL or any other * client side persistence. Passing `true` will use the default URL options. */ - routing?: RouterProps | boolean; + routing?: RouterProps | boolean; /** * the instance of search-insights to use for sending insights events inside @@ -136,7 +139,10 @@ export type InstantSearchOptions = { * created using the `instantsearch` factory function. * It emits the 'render' event every time a search is done */ -class InstantSearch extends EventEmitter { +class InstantSearch< + TUiState extends UiState = UiState, + TRouteState = TUiState +> extends EventEmitter { public client: InstantSearchOptions['searchClient']; public indexName: string; public insightsClient: AlgoliaInsightsClient | null; @@ -160,7 +166,7 @@ class InstantSearch extends EventEmitter { }> = []; public sendEventToInsights: (event: InsightsEvent) => void; - public constructor(options: InstantSearchOptions) { + public constructor(options: InstantSearchOptions) { super(); const { diff --git a/src/lib/__tests__/RoutingManager-test.ts b/src/lib/__tests__/RoutingManager-test.ts index 4f723acea6..4ad696efdd 100644 --- a/src/lib/__tests__/RoutingManager-test.ts +++ b/src/lib/__tests__/RoutingManager-test.ts @@ -4,7 +4,13 @@ import qs from 'qs'; import { createSearchClient } from '../../../test/mock/createSearchClient'; import { createWidget } from '../../../test/mock/createWidget'; import { runAllMicroTasks } from '../../../test/utils/runAllMicroTasks'; -import { Router, Widget, UiState, StateMapping, RouteState } from '../../types'; +import { + Router, + Widget, + UiState, + StateMapping, + IndexUiState, +} from '../../types'; import historyRouter from '../routers/history'; import instantsearch from '../main'; @@ -35,32 +41,30 @@ const createFakeStateMapping = ( ...args, }); -type Entry = Record; - -type HistoryState = { +type HistoryState = { index: number; - entries: Entry[]; - listeners: Array<(value: Entry) => void>; + entries: TEntry[]; + listeners: Array<(value: TEntry) => void>; }; -const createFakeHistory = ( +const createFakeHistory = >( { index = -1, entries = [], listeners = [], - }: HistoryState = {} as HistoryState + }: HistoryState = {} as HistoryState ) => { - const state: HistoryState = { + const state: HistoryState = { index, entries, listeners, }; return { - subscribe(listener: (entry: Entry) => void) { + subscribe(listener: (entry: TEntry) => void) { state.listeners.push(listener); }, - push(value: Entry) { + push(value: TEntry) { state.entries.push(value); state.index++; }, @@ -379,7 +383,7 @@ describe('RoutingManager', () => { test('should keep the UI state up to date on router.update', async () => { const searchClient = createSearchClient(); const stateMapping = createFakeStateMapping({}); - const history = createFakeHistory(); + const history = createFakeHistory(); const router = createFakeRouter({ onUpdate(fn) { history.subscribe(state => { @@ -471,7 +475,7 @@ describe('RoutingManager', () => { return uiState; }, }); - const history = createFakeHistory(); + const history = createFakeHistory(); const router = createFakeRouter({ onUpdate(fn) { history.subscribe(state => { @@ -549,10 +553,10 @@ describe('RoutingManager', () => { const searchClient = createSearchClient(); const stateMapping = createFakeStateMapping({}); const router = historyRouter({ - windowTitle(routeState: RouteState) { + windowTitle(routeState) { return `Searching for "${routeState.query}"`; }, - } as any); + }); const search = instantsearch({ indexName: 'instant_search', @@ -596,7 +600,7 @@ describe('RoutingManager', () => { url: createFakeUrlWithRefinements({ length: 22 }), }); - const router = historyRouter(); + const router = historyRouter(); // @ts-expect-error: This method is considered private but we still use it // in the test after the TypeScript migration. // In a next refactor, we can consider changing this test implementation. @@ -605,7 +609,7 @@ describe('RoutingManager', () => { location: window.location, }); - expect(parsedUrl.refinementList.brand).toBeInstanceOf(Array); + expect(parsedUrl.refinementList!.brand).toBeInstanceOf(Array); expect(parsedUrl).toMatchInlineSnapshot(` Object { "refinementList": Object { @@ -643,7 +647,7 @@ describe('RoutingManager', () => { url: createFakeUrlWithRefinements({ length: 100 }), }); - const router = historyRouter(); + const router = historyRouter(); // @ts-expect-error: This method is considered private but we still use it // in the test after the TypeScript migration. // In a next refactor, we can consider changing this test implementation. @@ -652,13 +656,13 @@ describe('RoutingManager', () => { location: window.location, }); - expect(parsedUrl.refinementList.brand).toBeInstanceOf(Array); + expect(parsedUrl.refinementList!.brand).toBeInstanceOf(Array); }); }); describe('createURL', () => { it('returns an URL for a `routeState` with refinements', () => { - const router = historyRouter(); + const router = historyRouter(); const actual = router.createURL({ query: 'iPhone', page: 5, diff --git a/src/lib/main.ts b/src/lib/main.ts index d0b4ef7eae..14b753d078 100644 --- a/src/lib/main.ts +++ b/src/lib/main.ts @@ -9,7 +9,7 @@ import * as middlewares from '../middlewares/index'; import * as routers from './routers/index'; import * as stateMappings from './stateMappings/index'; import { createInfiniteHitsSessionStorageCache } from './infiniteHitsCache/index'; -import { InstantSearchOptions } from '../types'; +import { InstantSearchOptions, UiState } from '../types'; /** * InstantSearch is the main component of InstantSearch.js. This object @@ -28,8 +28,12 @@ import { InstantSearchOptions } from '../types'; * @function instantsearch * @param {InstantSearchOptions} options The options */ -const instantsearch = (options: InstantSearchOptions) => - new InstantSearch(options); +const instantsearch = < + TUiState = Record, + TRouteState = TUiState +>( + options: InstantSearchOptions +) => new InstantSearch(options); instantsearch.routers = routers; instantsearch.stateMappings = stateMappings; diff --git a/src/lib/routers/__tests__/history.test.ts b/src/lib/routers/__tests__/history.test.ts index 43b2156061..cba4e93173 100644 --- a/src/lib/routers/__tests__/history.test.ts +++ b/src/lib/routers/__tests__/history.test.ts @@ -10,7 +10,7 @@ describe('life cycle', () => { it('writes after timeout is done', async () => { const pushState = jest.spyOn(window.history, 'pushState'); - const router = historyRouter({ + const router = historyRouter<{ some: string }>({ writeDelay: 0, }); diff --git a/src/lib/routers/history.ts b/src/lib/routers/history.ts index 582a44069d..c41cca604a 100644 --- a/src/lib/routers/history.ts +++ b/src/lib/routers/history.ts @@ -1,56 +1,22 @@ import qs from 'qs'; -import { Router, RouteState } from '../../types'; +import { Router, UiState } from '../../types'; -type CreateURL = ({ - qsModule, - routeState, - location, -}: { +type CreateURL = (args: { qsModule: typeof qs; - routeState: RouteState; + routeState: TRouteState; location: Location; }) => string; -type ParseURL = ({ - qsModule, - location, -}: { +type ParseURL = (args: { qsModule: typeof qs; location: Location; -}) => RouteState; +}) => TRouteState; -type BrowserHistoryArgs = { - windowTitle?: (routeState: RouteState) => string; - writeDelay?: number; - createURL?: CreateURL; - parseURL?: ParseURL; -}; - -const defaultCreateURL: CreateURL = ({ qsModule, routeState, location }) => { - const { protocol, hostname, port = '', pathname, hash } = location; - const queryString = qsModule.stringify(routeState); - const portWithPrefix = port === '' ? '' : `:${port}`; - - // IE <= 11 has no proper `location.origin` so we cannot rely on it. - if (!queryString) { - return `${protocol}//${hostname}${portWithPrefix}${pathname}${hash}`; - } - - return `${protocol}//${hostname}${portWithPrefix}${pathname}?${queryString}${hash}`; -}; - -const defaultParseURL: ParseURL = ({ qsModule, location }) => { - // `qs` by default converts arrays with more than 20 items to an object. - // We want to avoid this because the data structure manipulated can therefore vary. - // Setting the limit to `100` seems a good number because the engine's default is 100 - // (it can go up to 1000 but it is very unlikely to select more than 100 items in the UI). - // - // Using an `arrayLimit` of `n` allows `n + 1` items. - // - // See: - // - https://github.com/ljharb/qs#parsing-arrays - // - https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/ - return qsModule.parse(location.search.slice(1), { arrayLimit: 99 }); +type BrowserHistoryArgs = { + windowTitle?: (routeState: TRouteState) => string; + writeDelay: number; + createURL: CreateURL; + parseURL: ParseURL; }; const setWindowTitle = (title?: string): void => { @@ -59,11 +25,11 @@ const setWindowTitle = (title?: string): void => { } }; -class BrowserHistory implements Router { +class BrowserHistory implements Router { /** * Transforms a UI state into a title for the page. */ - private readonly windowTitle?: BrowserHistoryArgs['windowTitle']; + private readonly windowTitle?: BrowserHistoryArgs['windowTitle']; /** * Time in milliseconds before performing a write in the history. * It prevents from adding too many entries in the history and @@ -71,17 +37,23 @@ class BrowserHistory implements Router { * * @default 400 */ - private readonly writeDelay: Required['writeDelay']; + private readonly writeDelay: Required< + BrowserHistoryArgs + >['writeDelay']; /** * Creates a full URL based on the route state. * The storage adaptor maps all syncable keys to the query string of the URL. */ - private readonly _createURL: Required['createURL']; + private readonly _createURL: Required< + BrowserHistoryArgs + >['createURL']; /** * Parses the URL into a route state. - * It should be symetrical to `createURL`. + * It should be symmetrical to `createURL`. */ - private readonly parseURL: Required['parseURL']; + private readonly parseURL: Required< + BrowserHistoryArgs + >['parseURL']; private writeTimer?: number; private _onPopState?(event: PopStateEvent): void; @@ -90,14 +62,12 @@ class BrowserHistory implements Router { * Initializes a new storage provider that syncs the search state to the URL * using web APIs (`window.location.pushState` and `onpopstate` event). */ - public constructor( - { - windowTitle, - writeDelay = 400, - createURL = defaultCreateURL, - parseURL = defaultParseURL, - }: BrowserHistoryArgs = {} as BrowserHistoryArgs - ) { + public constructor({ + windowTitle, + writeDelay = 400, + createURL, + parseURL, + }: BrowserHistoryArgs) { this.windowTitle = windowTitle; this.writeTimer = undefined; this.writeDelay = writeDelay; @@ -112,14 +82,14 @@ class BrowserHistory implements Router { /** * Reads the URL and returns a syncable UI search state. */ - public read(): RouteState { + public read(): TRouteState { return this.parseURL({ qsModule: qs, location: window.location }); } /** * Pushes a search state into the URL. */ - public write(routeState: RouteState): void { + public write(routeState: TRouteState): void { const url = this.createURL(routeState); const title = this.windowTitle && this.windowTitle(routeState); @@ -138,7 +108,7 @@ class BrowserHistory implements Router { * Sets a callback on the `onpopstate` event of the history API of the current page. * It enables the URL sync to keep track of the changes. */ - public onUpdate(callback: (routeState: RouteState) => void): void { + public onUpdate(callback: (routeState: TRouteState) => void): void { this._onPopState = event => { if (this.writeTimer) { window.clearTimeout(this.writeTimer); @@ -167,7 +137,7 @@ class BrowserHistory implements Router { * This allows to handle cases like using a . * See: https://github.com/algolia/instantsearch.js/issues/790 */ - public createURL(routeState: RouteState): string { + public createURL(routeState: TRouteState): string { return this._createURL({ qsModule: qs, routeState, @@ -187,10 +157,43 @@ class BrowserHistory implements Router { window.clearTimeout(this.writeTimer); } - this.write({}); + this.write({} as TRouteState); } } -export default function(props?: BrowserHistoryArgs): BrowserHistory { - return new BrowserHistory(props); +export default function historyRouter({ + createURL = ({ qsModule, routeState, location }) => { + const { protocol, hostname, port = '', pathname, hash } = location; + const queryString = qsModule.stringify(routeState); + const portWithPrefix = port === '' ? '' : `:${port}`; + + // IE <= 11 has no proper `location.origin` so we cannot rely on it. + if (!queryString) { + return `${protocol}//${hostname}${portWithPrefix}${pathname}${hash}`; + } + + return `${protocol}//${hostname}${portWithPrefix}${pathname}?${queryString}${hash}`; + }, + parseURL = ({ qsModule, location }) => { + // `qs` by default converts arrays with more than 20 items to an object. + // We want to avoid this because the data structure manipulated can therefore vary. + // Setting the limit to `100` seems a good number because the engine's default is 100 + // (it can go up to 1000 but it is very unlikely to select more than 100 items in the UI). + // + // Using an `arrayLimit` of `n` allows `n + 1` items. + // + // See: + // - https://github.com/ljharb/qs#parsing-arrays + // - https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/ + return qsModule.parse(location.search.slice(1), { arrayLimit: 99 }); + }, + writeDelay = 400, + windowTitle, +}: Partial> = {}): BrowserHistory { + return new BrowserHistory({ + createURL, + parseURL, + writeDelay, + windowTitle, + }); } diff --git a/src/lib/stateMappings/__tests__/simple-test.ts b/src/lib/stateMappings/__tests__/simple-test.ts index 14e727ce38..f55f863c5c 100644 --- a/src/lib/stateMappings/__tests__/simple-test.ts +++ b/src/lib/stateMappings/__tests__/simple-test.ts @@ -1,3 +1,4 @@ +import { UiState } from '../../../types'; import simpleStateMapping from '../simple'; describe('simpleStateMapping', () => { @@ -119,7 +120,9 @@ describe('simpleStateMapping', () => { }); it('passes non-UiState through', () => { - const stateMapping = simpleStateMapping(); + const stateMapping = simpleStateMapping< + UiState & { [indexId: string]: { spy: string[] } } + >(); const actual = stateMapping.routeToState({ indexName: { query: 'zamboni', diff --git a/src/lib/stateMappings/__tests__/singleIndex-test.ts b/src/lib/stateMappings/__tests__/singleIndex-test.ts index eb0b905a19..89b5210e0b 100644 --- a/src/lib/stateMappings/__tests__/singleIndex-test.ts +++ b/src/lib/stateMappings/__tests__/singleIndex-test.ts @@ -1,3 +1,4 @@ +import { UiState } from '../../../types'; import singleIndexStateMapping from '../singleIndex'; describe('singleIndexStateMapping', () => { @@ -156,7 +157,9 @@ describe('singleIndexStateMapping', () => { }); it('passes non-UiState through', () => { - const stateMapping = singleIndexStateMapping('indexName'); + const stateMapping = singleIndexStateMapping< + UiState & { [indexName: string]: { spy: string[] } } + >('indexName'); const actual = stateMapping.routeToState({ query: 'zamboni', refinementList: { @@ -179,6 +182,7 @@ describe('singleIndexStateMapping', () => { it('returns wrong data if used with nested state', () => { const stateMapping = singleIndexStateMapping('indexName'); const actual = stateMapping.routeToState({ + // @ts-expect-error indexName: { query: 'zamboni', refinementList: { diff --git a/src/lib/stateMappings/simple.ts b/src/lib/stateMappings/simple.ts index 92f458e5e0..c303956623 100644 --- a/src/lib/stateMappings/simple.ts +++ b/src/lib/stateMappings/simple.ts @@ -1,6 +1,8 @@ -import { UiState, IndexUiState, StateMapping, RouteState } from '../../types'; +import { UiState, IndexUiState, StateMapping } from '../../types'; -function getIndexStateWithoutConfigure(uiState: IndexUiState): IndexUiState { +function getIndexStateWithoutConfigure( + uiState: TIndexUiState +): Omit { const { configure, ...trackedUiState } = uiState; return trackedUiState; } @@ -8,25 +10,27 @@ function getIndexStateWithoutConfigure(uiState: IndexUiState): IndexUiState { // technically a URL could contain any key, since users provide it, // which is why the input to this function is UiState, not something // which excludes "configure" as this function does. -export default function simpleStateMapping(): StateMapping { +export default function simpleStateMapping< + TUiState extends UiState = UiState +>(): StateMapping { return { stateToRoute(uiState) { - return Object.keys(uiState).reduce( + return Object.keys(uiState).reduce( (state, indexId) => ({ ...state, [indexId]: getIndexStateWithoutConfigure(uiState[indexId]), }), - {} + {} as TUiState ); }, - routeToState(routeState = {}) { - return Object.keys(routeState).reduce( + routeToState(routeState = {} as TUiState) { + return Object.keys(routeState).reduce( (state, indexId) => ({ ...state, [indexId]: getIndexStateWithoutConfigure(routeState[indexId]), }), - {} + {} as TUiState ); }, }; diff --git a/src/lib/stateMappings/singleIndex.ts b/src/lib/stateMappings/singleIndex.ts index df07b817c5..5e02446a7c 100644 --- a/src/lib/stateMappings/singleIndex.ts +++ b/src/lib/stateMappings/singleIndex.ts @@ -1,21 +1,25 @@ -import { StateMapping, IndexUiState } from '../../types'; +import { StateMapping, IndexUiState, UiState } from '../../types'; -function getIndexStateWithoutConfigure(uiState: IndexUiState): IndexUiState { +function getIndexStateWithoutConfigure( + uiState: TIndexUiState +): TIndexUiState { const { configure, ...trackedUiState } = uiState; - return trackedUiState; + return trackedUiState as TIndexUiState; } -export default function singleIndexStateMapping( - indexName: string -): StateMapping { +export default function singleIndexStateMapping< + TUiState extends UiState = UiState +>( + indexName: keyof TUiState +): StateMapping { return { stateToRoute(uiState) { return getIndexStateWithoutConfigure(uiState[indexName] || {}); }, - routeToState(routeState = {}) { - return { + routeToState(routeState = {} as TUiState[typeof indexName]) { + return ({ [indexName]: getIndexStateWithoutConfigure(routeState), - }; + } as unknown) as TUiState; }, }; } diff --git a/src/middlewares/createRouterMiddleware.ts b/src/middlewares/createRouterMiddleware.ts index a287af7fe7..b67950ed00 100644 --- a/src/middlewares/createRouterMiddleware.ts +++ b/src/middlewares/createRouterMiddleware.ts @@ -5,31 +5,48 @@ import { StateMapping, UiState, InternalMiddleware, - RouteState, + CreateURL, } from '../types'; import { isEqual } from '../lib/utils'; -export type RouterProps = { - router?: Router; - stateMapping?: StateMapping; +export type RouterProps< + TUiState extends UiState = UiState, + TRouteState = TUiState +> = { + router?: Router; + // ideally stateMapping should be required if TRouteState is given, + // but there's no way to check if a generic is provided or the default value. + stateMapping?: StateMapping; }; -export type RoutingManager = (props?: RouterProps) => InternalMiddleware; - -export const createRouterMiddleware: RoutingManager = (props = {}) => { +export const createRouterMiddleware = < + TUiState extends UiState = UiState, + TRouteState = TUiState +>( + props: RouterProps = {} +): InternalMiddleware => { const { - router = historyRouter(), - stateMapping = simpleStateMapping(), + router = historyRouter(), + // We have to cast simpleStateMapping as a StateMapping. + // this is needed because simpleStateMapping is StateMapping. + // While it's only used when UiState and RouteState are the same, unfortunately + // TypeScript still considers them separate types. + stateMapping = (simpleStateMapping() as unknown) as StateMapping< + TUiState, + TRouteState + >, } = props; return ({ instantSearchInstance }) => { - function topLevelCreateURL(nextState: UiState) { - const uiState: UiState = Object.keys(nextState).reduce( + function topLevelCreateURL(nextState: TUiState) { + const uiState: TUiState = Object.keys(nextState).reduce( (acc, indexId) => ({ ...acc, [indexId]: nextState[indexId], }), - instantSearchInstance.mainIndex.getWidgetUiState({}) + instantSearchInstance.mainIndex.getWidgetUiState( + {} as TUiState + ) ); const route = stateMapping.stateToRoute(uiState); @@ -37,13 +54,15 @@ export const createRouterMiddleware: RoutingManager = (props = {}) => { return router.createURL(route); } - instantSearchInstance._createURL = topLevelCreateURL; + // casting to UiState here to keep createURL unaware of custom UiState + // (as long as it's an object, it's ok) + instantSearchInstance._createURL = topLevelCreateURL as CreateURL; instantSearchInstance._initialUiState = { ...instantSearchInstance._initialUiState, ...stateMapping.routeToState(router.read()), }; - let lastRouteState: RouteState | undefined = undefined; + let lastRouteState: TRouteState | undefined = undefined; return { onStateChange({ uiState }) { diff --git a/src/types/middleware.ts b/src/types/middleware.ts index d9b82b76c7..e46ba9d9db 100644 --- a/src/types/middleware.ts +++ b/src/types/middleware.ts @@ -2,8 +2,8 @@ import InstantSearch from '../lib/InstantSearch'; import { UiState } from './ui-state'; import { AtLeastOne } from './utils'; -export type MiddlewareDefinition = { - onStateChange(options: { uiState: UiState }): void; +export type MiddlewareDefinition = { + onStateChange(options: { uiState: TUiState }): void; subscribe(): void; unsubscribe(): void; }; @@ -12,9 +12,9 @@ export type MiddlewareOptions = { instantSearchInstance: InstantSearch; }; -export type InternalMiddleware = ( +export type InternalMiddleware = ( options: MiddlewareOptions -) => MiddlewareDefinition; +) => MiddlewareDefinition; export type Middleware = ( options: MiddlewareOptions diff --git a/src/types/router.ts b/src/types/router.ts index 0cfab9df4d..11e1b1cc0d 100644 --- a/src/types/router.ts +++ b/src/types/router.ts @@ -4,32 +4,32 @@ import { UiState } from './ui-state'; * The router is the part that saves and reads the object from the storage. * Usually this is the URL. */ -export type Router = { +export type Router = { /** * onUpdate Sets an event listener that is triggered when the storage is updated. * The function should accept a callback to trigger when the update happens. * In the case of the history / URL in a browser, the callback will be called * by `onPopState`. */ - onUpdate(callback: (route: RouteState) => void): void; + onUpdate(callback: (route: TRouteState) => void): void; /** * Reads the storage and gets a route object. It does not take parameters, * and should return an object */ - read(): RouteState; + read(): TRouteState; /** * Pushes a route object into a storage. Takes the UI state mapped by the state * mapping configured in the mapping */ - write(route: RouteState): void; + write(route: TRouteState): void; /** * Transforms a route object into a URL. It receives an object and should * return a string. It may return an empty string. */ - createURL(state: RouteState): string; + createURL(state: TRouteState): string; /** * Called when InstantSearch is disposed. Used to remove subscriptions. @@ -43,23 +43,18 @@ export type Router = { * should be valid for any UiState: * `UiState = routeToState(stateToRoute(UiState))`. */ -export type StateMapping = { +export type StateMapping = { /** * Transforms a UI state representation into a route object. * It receives an object that contains the UI state of all the widgets in the page. * It should return an object of any form as long as this form can be read by * the `routeToState` function. */ - stateToRoute(uiState: UiState): RouteState; + stateToRoute(uiState: TUiState): TRouteState; /** * Transforms route object into a UI state representation. * It receives an object that contains the UI state stored by the router. * The format is the output of `stateToRoute`. */ - routeToState(routeState: RouteState): UiState; -}; - -// @TODO: use the generic form of this in routers -export type RouteState = { - [stateKey: string]: any; + routeToState(routeState: TRouteState): TUiState; }; diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts index 8b02aeba6c..e5b8ae3d9e 100644 --- a/src/widgets/index/index.ts +++ b/src/widgets/index/index.ts @@ -76,7 +76,7 @@ export type IndexWidget = Omit< * @deprecated */ getWidgetState(uiState: UiState): UiState; - getWidgetUiState(uiState: UiState): UiState; + getWidgetUiState(uiState: TUiState): TUiState; getWidgetSearchParameters( searchParameters: SearchParameters, searchParametersOptions: { uiState: IndexUiState } @@ -677,10 +677,10 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => { derivedHelper = null; }, - getWidgetUiState(uiState: UiState) { + getWidgetUiState(uiState: TUiState) { return localWidgets .filter(isIndexWidget) - .reduce( + .reduce( (previousUiState, innerIndex) => innerIndex.getWidgetUiState(previousUiState), {