Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ts): allow custom ui state and route state in routing #4816

Merged
merged 6 commits into from
Aug 4, 2021
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
10 changes: 7 additions & 3 deletions src/index.es.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InstantSearchOptions } from './types';
import { InstantSearchOptions, UiState } from './types';
import InstantSearch from './lib/InstantSearch';
import version from './lib/version';
import {
Expand All @@ -11,8 +11,12 @@ import {
} from './helpers';
import { createInfiniteHitsSessionStorageCache } from './lib/infiniteHitsCache';

const instantsearch = (options: InstantSearchOptions): InstantSearch =>
new InstantSearch(options);
const instantsearch = <
TUiState = Record<string, unknown>,
TRouteState = TUiState
>(
options: InstantSearchOptions<UiState & TUiState, TRouteState>
) => new InstantSearch(options);

instantsearch.version = version;
instantsearch.snippet = snippet;
Expand Down
14 changes: 10 additions & 4 deletions src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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<TUiState, TRouteState> | boolean;

/**
* the instance of search-insights to use for sending insights events inside
Expand All @@ -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;
Expand All @@ -160,7 +166,7 @@ class InstantSearch extends EventEmitter {
}> = [];
public sendEventToInsights: (event: InsightsEvent) => void;

public constructor(options: InstantSearchOptions) {
public constructor(options: InstantSearchOptions<TUiState, TRouteState>) {
super();

const {
Expand Down
44 changes: 24 additions & 20 deletions src/lib/__tests__/RoutingManager-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -35,32 +41,30 @@ const createFakeStateMapping = (
...args,
});

type Entry = Record<string, unknown>;

type HistoryState = {
type HistoryState<TEntry> = {
index: number;
entries: Entry[];
listeners: Array<(value: Entry) => void>;
entries: TEntry[];
listeners: Array<(value: TEntry) => void>;
};

const createFakeHistory = (
const createFakeHistory = <TEntry = Record<string, unknown>>(
{
index = -1,
entries = [],
listeners = [],
}: HistoryState = {} as HistoryState
}: HistoryState<TEntry> = {} as HistoryState<TEntry>
) => {
const state: HistoryState = {
const state: HistoryState<TEntry> = {
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++;
},
Expand Down Expand Up @@ -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<UiState>();
const router = createFakeRouter({
onUpdate(fn) {
history.subscribe(state => {
Expand Down Expand Up @@ -471,7 +475,7 @@ describe('RoutingManager', () => {
return uiState;
},
});
const history = createFakeHistory();
const history = createFakeHistory<UiState>();
const router = createFakeRouter({
onUpdate(fn) {
history.subscribe(state => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -596,7 +600,7 @@ describe('RoutingManager', () => {
url: createFakeUrlWithRefinements({ length: 22 }),
});

const router = historyRouter();
const router = historyRouter<IndexUiState>();
// @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.
Expand All @@ -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 {
Expand Down Expand Up @@ -643,7 +647,7 @@ describe('RoutingManager', () => {
url: createFakeUrlWithRefinements({ length: 100 }),
});

const router = historyRouter();
const router = historyRouter<IndexUiState>();
// @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.
Expand All @@ -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<IndexUiState>();
const actual = router.createURL({
query: 'iPhone',
page: 5,
Expand Down
10 changes: 7 additions & 3 deletions src/lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, unknown>,
TRouteState = TUiState
>(
options: InstantSearchOptions<UiState & TUiState, TRouteState>
) => new InstantSearch(options);

instantsearch.routers = routers;
instantsearch.stateMappings = stateMappings;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/routers/__tests__/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
Loading