Skip to content

Commit

Permalink
feat(index): replicate searchFunction hack (#4078)
Browse files Browse the repository at this point in the history
This PR replicates [the hack](https://github.com/algolia/instantsearch.js/blob/509513c0feafaad522f6f18d87a441559f4aa050/src/lib/RoutingManager.ts#L113-L130) implemented inside `RoutingManager` for the `searchFunction`. The `uiState` is now a first-class citizen so we have to apply this logic to the index. I've tried to keep the same process: the update is triggered once all the widgets of the index are rendered like it was the case inside the manager (since it was always the last widget).

At the moment the call to notify the instance that a change occurs isn't implemented because we don't expose the function (yet). Once the API is available I'll update the PR. The pieces that need to be updated are commented.
  • Loading branch information
samouss authored and Haroenv committed Oct 23, 2019
1 parent fe23c55 commit 1d2a816
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 24 deletions.
199 changes: 175 additions & 24 deletions src/widgets/index/__tests__/index-test.ts
Expand Up @@ -31,10 +31,7 @@ describe('index', () => {
};
}),
getWidgetSearchParameters: jest.fn((searchParameters, { uiState }) => {
return searchParameters.setQueryParameter(
'query',
uiState.query || 'Apple'
);
return searchParameters.setQueryParameter('query', uiState.query || '');
}),
...args,
});
Expand All @@ -55,7 +52,7 @@ describe('index', () => {
};
}),
getWidgetSearchParameters: jest.fn((searchParameters, { uiState }) => {
return searchParameters.setQueryParameter('page', uiState.page || 5);
return searchParameters.setQueryParameter('page', uiState.page || 0);
}),
...args,
});
Expand Down Expand Up @@ -139,22 +136,24 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
});

it('returns the instance to be able to chain the calls', () => {
const topLevelIndex = index({ indexName: 'topLevelIndexName' });
const subLevelIndex = index({ indexName: 'subLevelIndexName' });
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });

topLevelIndex.addWidgets([
subLevelIndex.addWidgets([createSearchBox(), createPagination()]),
topLevelInstance.addWidgets([
subLevelInstance.addWidgets([createSearchBox(), createPagination()]),
]);

expect(topLevelIndex.getWidgets()).toHaveLength(1);
expect(topLevelIndex.getWidgets()).toEqual([subLevelIndex]);
expect(topLevelInstance.getWidgets()).toHaveLength(1);
expect(topLevelInstance.getWidgets()).toEqual([subLevelInstance]);
});

it('does not throw an error without the `init` step', () => {
const topLevelIndex = index({ indexName: 'topLevelIndexName' });
const subLevelIndex = index({ indexName: 'subLevelIndexName' });
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });

expect(() => topLevelIndex.addWidgets([subLevelIndex])).not.toThrow();
expect(() =>
topLevelInstance.addWidgets([subLevelInstance])
).not.toThrow();
});

it('throws an error with a value different than `array`', () => {
Expand Down Expand Up @@ -190,7 +189,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
it('updates the internal state with added widgets', () => {
const instance = index({ indexName: 'indexName' });

instance.addWidgets([createSearchBox()]);
instance.addWidgets([
createSearchBox({
getWidgetSearchParameters(state) {
return state.setQueryParameter('query', 'Apple');
},
}),
]);

instance.init(createInitOptions({ parent: null }));

Expand All @@ -201,7 +206,13 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
})
);

instance.addWidgets([createPagination()]);
instance.addWidgets([
createPagination({
getWidgetSearchParameters(state) {
return state.setQueryParameter('page', 5);
},
}),
]);

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
Expand Down Expand Up @@ -333,12 +344,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"

it('does not throw an error without the `init` step', () => {
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelIndex = index({ indexName: 'subLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });

topLevelInstance.addWidgets([subLevelIndex]);
topLevelInstance.addWidgets([subLevelInstance]);

expect(() =>
topLevelInstance.removeWidgets([subLevelIndex])
topLevelInstance.removeWidgets([subLevelInstance])
).not.toThrow();
});

Expand Down Expand Up @@ -374,9 +385,20 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
describe('with a started instance', () => {
it('updates the internal state with removed widgets', () => {
const instance = index({ indexName: 'indexName' });
const pagination = createPagination();
const pagination = createPagination({
getWidgetSearchParameters(state) {
return state.setQueryParameter('page', 5);
},
});

instance.addWidgets([createSearchBox(), pagination]);
instance.addWidgets([
createSearchBox({
getWidgetSearchParameters(state) {
return state.setQueryParameter('query', 'Apple');
},
}),
pagination,
]);

instance.init(createInitOptions({ parent: null }));

Expand Down Expand Up @@ -623,7 +645,18 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
mainHelper,
});

instance.addWidgets([createSearchBox(), createPagination()]);
instance.addWidgets([
createSearchBox({
getWidgetSearchParameters(state) {
return state.setQueryParameter('query', 'Apple');
},
}),
createPagination({
getWidgetSearchParameters(state) {
return state.setQueryParameter('page', 5);
},
}),
]);

instance.init(
createInitOptions({
Expand Down Expand Up @@ -751,7 +784,18 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
mainHelper,
});

instance.addWidgets([createSearchBox(), createPagination()]);
instance.addWidgets([
createSearchBox({
getWidgetSearchParameters(state) {
return state.setQueryParameter('query', 'Apple');
},
}),
createPagination({
getWidgetSearchParameters(state) {
return state.setQueryParameter('page', 5);
},
}),
]);

instance.init(
createInitOptions({
Expand Down Expand Up @@ -1483,7 +1527,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
expect(instantSearchInstance.onStateChange).not.toHaveBeenCalled();
});

it('updates the local `uiState` only with widgets not indices', () => {
it('updates the local `uiState` only with widgets', () => {
const level0 = index({ indexName: 'level0IndexName' });
const level1 = index({ indexName: 'level1IndexName' });
const widgets = [createSearchBox(), createPagination()];
Expand All @@ -1507,6 +1551,113 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index/js/"
expect(level1.getWidgetState).toHaveBeenCalledTimes(0);
});

it('updates the local `uiState` when they differ on first render', () => {
const instance = index({ indexName: 'indexName' });
const instantSearchInstance = createInstantSearch({
onStateChange: jest.fn(),
});

instance.addWidgets([createSearchBox()]);

instance.init(
createInitOptions({
instantSearchInstance,
parent: null,
})
);

expect(instance.getWidgetState({})).toEqual({
indexName: {},
});

// Simulate a change that does not emit (like `searchFunction`)
instance.getHelper()!.overrideStateWithoutTriggeringChangeEvent({
...instance.getHelper()!.state,
query: 'Apple iPhone',
});

instance.render(
createRenderOptions({
instantSearchInstance,
})
);

expect(instantSearchInstance.onStateChange).toHaveBeenCalledTimes(1);
expect(instance.getWidgetState({})).toEqual({
indexName: {
query: 'Apple iPhone',
},
});

// Simulate a change that does not emit (like `searchFunction`)
instance.getHelper()!.overrideStateWithoutTriggeringChangeEvent({
...instance.getHelper()!.state,
query: 'Apple iPhone XS',
});

instance.render(
createRenderOptions({
instantSearchInstance,
})
);

expect(instantSearchInstance.onStateChange).toHaveBeenCalledTimes(1);
expect(instance.getWidgetState({})).toEqual({
indexName: {
query: 'Apple iPhone',
},
});
});

it('does not update the local `uiState` on first render for children indices', async () => {
const topLevelInstance = index({ indexName: 'topLevelIndexName' });
const subLevelInstance = index({ indexName: 'subLevelIndexName' });
const instantSearchInstance = createInstantSearch({
onStateChange: jest.fn(),
});

topLevelInstance.addWidgets([
createSearchBox(),
subLevelInstance.addWidgets([createSearchBox()]),
]);

topLevelInstance.init(
createInitOptions({
instantSearchInstance,
parent: null,
})
);

expect(subLevelInstance.getWidgetState({})).toEqual({
subLevelIndexName: {},
});

subLevelInstance
.getHelper()!
// Simulate a change that does not emit (like `searchFunction`)
.overrideStateWithoutTriggeringChangeEvent({
...subLevelInstance.getHelper()!.state,
query: 'Apple iPhone',
})
// Simulate a call to search from a widget - this step is required otherwise
// the DerivedHelper does not contain the results. The `lastResults` attribute
// is set once the `result` event is emitted.
.search();

await runAllMicroTasks();

topLevelInstance.render(
createRenderOptions({
instantSearchInstance,
})
);

expect(instantSearchInstance.onStateChange).not.toHaveBeenCalled();
expect(subLevelInstance.getWidgetState({})).toEqual({
subLevelIndexName: {},
});
});

it('retrieves the `uiState` for the children indices', () => {
const level0 = index({ indexName: 'level0IndexName' });
const level1 = index({ indexName: 'level1IndexName' });
Expand Down
26 changes: 26 additions & 0 deletions src/widgets/index/index.ts
Expand Up @@ -21,6 +21,7 @@ import {
createDocumentationMessageGenerator,
resolveSearchParameters,
mergeSearchParameters,
isEqual,
} from '../../lib/utils';

const withUsage = createDocumentationMessageGenerator({
Expand Down Expand Up @@ -147,6 +148,7 @@ const index = (props: IndexProps): Index => {
let localParent: Index | null = null;
let helper: Helper | null = null;
let derivedHelper: DerivedHelper | null = null;
let isFirstRender = true;

return {
$$type: 'ais.index',
Expand Down Expand Up @@ -380,6 +382,7 @@ const index = (props: IndexProps): Index => {
searchParameters: state,
helper: helper!,
});

instantSearchInstance.onStateChange();
});
},
Expand Down Expand Up @@ -408,6 +411,29 @@ const index = (props: IndexProps): Index => {
});
}
});

// Hack for backward compatibily with `searchFunction` + `routing`
// https://github.com/algolia/instantsearch.js/blob/509513c0feafaad522f6f18d87a441559f4aa050/src/lib/RoutingManager.ts#L113-L130
if (localParent === null && isFirstRender) {
isFirstRender = false;
// Compare initial state and first render state to see if the query has been
// changed by the `searchFunction`. It's required because the helper of the
// `searchFunction` does not trigger change events (not the same instance).
const firstRenderState = getLocalWidgetsState(localWidgets, {
helper: helper!,
searchParameters: helper!.state,
});

if (!isEqual(localUiState, firstRenderState)) {
// Force update the URL if the state has changed since the initial read.
// We do this to trigger a URL update when `searchFunction` prevents
// the search on the initial render.
// See: https://github.com/algolia/instantsearch.js/issues/2523#issuecomment-339356157
localUiState = firstRenderState;

instantSearchInstance.onStateChange();
}
}
},

dispose() {
Expand Down

0 comments on commit 1d2a816

Please sign in to comment.