= {}): Widget =>
@@ -651,6 +652,236 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
});
});
+ describe('createURL', () => {
+ it('default url returns #', () => {
+ const instance = index({ indexName: 'indexName' });
+ const searchBox = createSearchBox();
+ const pagination = createPagination();
+
+ instance.addWidgets([searchBox, pagination]);
+
+ instance.init(createInitOptions());
+
+ expect(instance.createURL(new SearchParameters())).toEqual('#');
+ });
+
+ it('calls the createURL of routing', () => {
+ const instance = index({ indexName: 'indexName' });
+ const searchBox = createSearchBox();
+ const pagination = createPagination();
+
+ instance.addWidgets([searchBox, pagination]);
+
+ instance.init(
+ createInitOptions({
+ instantSearchInstance: createInstantSearch({
+ // @ts-ignore
+ _createURL(routeState) {
+ return routeState;
+ },
+ }),
+ })
+ );
+
+ expect(instance.createURL(new SearchParameters())).toEqual({
+ indexName: {},
+ });
+ });
+
+ it('create URLs with custom helper state', () => {
+ const instance = index({ indexName: 'indexName' });
+ const searchBox = createSearchBox();
+ const pagination = createPagination();
+
+ instance.addWidgets([searchBox, pagination]);
+
+ instance.init(
+ createInitOptions({
+ instantSearchInstance: createInstantSearch({
+ // @ts-ignore
+ _createURL(routeState) {
+ return routeState;
+ },
+ }),
+ })
+ );
+
+ expect(instance.createURL(new SearchParameters({ page: 100 }))).toEqual({
+ indexName: { page: 100 },
+ });
+ });
+
+ it('create URLs with non-namesake helper state', () => {
+ const instance = index({ indexName: 'indexName' });
+ const searchBox = createSearchBox();
+ const pagination = createPagination();
+
+ const container = document.createElement('div');
+ document.body.append(container);
+
+ instance.addWidgets([
+ searchBox,
+ pagination,
+ refinementList({ container, attribute: 'doggies' }),
+ ]);
+
+ instance.init(
+ createInitOptions({
+ instantSearchInstance: createInstantSearch({
+ // @ts-ignore
+ _createURL(routeState) {
+ return routeState;
+ },
+ }),
+ })
+ );
+
+ expect(
+ instance.createURL(
+ new SearchParameters({
+ disjunctiveFacets: ['doggies'],
+ disjunctiveFacetsRefinements: { doggies: ['zap'] },
+ })
+ )
+ ).toEqual({
+ indexName: { refinementList: { doggies: ['zap'] } },
+ });
+ });
+ });
+
+ describe('getScopedResults', () => {
+ it('gets deep results', async () => {
+ const level0 = index({ indexName: 'level0IndexName' });
+ const level1 = index({ indexName: 'level1IndexName' });
+ const level2 = index({ indexName: 'level2IndexName' });
+ const level21 = index({ indexName: 'level21IndexName' });
+ const level22 = index({ indexName: 'level22IndexName' });
+ const level221 = index({ indexName: 'level221IndexName' });
+ const level3 = index({ indexName: 'level3IndexName' });
+ const searchBoxLevel0 = createSearchBox();
+ const searchBoxLevel1 = createSearchBox();
+ const searchBoxLevel21 = createSearchBox();
+
+ level0.addWidgets([
+ searchBoxLevel0,
+ level1.addWidgets([searchBoxLevel1]),
+ level2.addWidgets([
+ createSearchBox(),
+ level21.addWidgets([searchBoxLevel21]),
+ level22.addWidgets([
+ createSearchBox(),
+ level221.addWidgets([createSearchBox()]),
+ ]),
+ ]),
+ level3.addWidgets([createSearchBox()]),
+ ]);
+
+ level0.init(createInitOptions({ parent: null }));
+
+ // 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.
+ level0.getHelper()!.search();
+
+ await runAllMicroTasks();
+
+ expect(level1.getScopedResults()).toEqual([
+ // Root index
+ {
+ indexId: 'level1IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level1.getHelper(),
+ },
+ // Siblings and children
+ {
+ indexId: 'level2IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level2.getHelper(),
+ },
+ {
+ indexId: 'level21IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level21.getHelper(),
+ },
+ {
+ indexId: 'level22IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level22.getHelper(),
+ },
+ {
+ indexId: 'level221IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level221.getHelper(),
+ },
+ {
+ indexId: 'level3IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level3.getHelper(),
+ },
+ ]);
+
+ expect(level21.getScopedResults()).toEqual([
+ // Root index
+ {
+ indexId: 'level21IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level21.getHelper(),
+ },
+ // Siblings and children
+ {
+ indexId: 'level22IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level22.getHelper(),
+ },
+ {
+ indexId: 'level221IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level221.getHelper(),
+ },
+ ]);
+
+ expect(level0.getScopedResults()).toEqual([
+ // Root index
+ {
+ indexId: 'level0IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level0.getHelper(),
+ },
+ // Siblings and children
+ {
+ indexId: 'level1IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level1.getHelper(),
+ },
+ {
+ indexId: 'level2IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level2.getHelper(),
+ },
+ {
+ indexId: 'level21IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level21.getHelper(),
+ },
+ {
+ indexId: 'level22IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level22.getHelper(),
+ },
+ {
+ indexId: 'level221IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level221.getHelper(),
+ },
+ {
+ indexId: 'level3IndexName',
+ results: expect.any(algoliasearchHelper.SearchResults),
+ helper: level3.getHelper(),
+ },
+ ]);
+ });
+ });
+
describe('init', () => {
it('forwards the `search` call to the main instance', () => {
const instance = index({ indexName: 'indexName' });
@@ -2579,5 +2810,29 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
'[InstantSearch.js]: The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.'
);
});
+
+ test('does not warn for index itself', () => {
+ warning.cache = {};
+
+ const instance = index({ indexName: 'indexName' });
+ const searchClient = createSearchClient();
+ const mainHelper = algoliasearchHelper(searchClient, '', {});
+ const instantSearchInstance = createInstantSearch({
+ mainHelper,
+ });
+
+ instance.addWidgets([index({ indexName: 'other' })]);
+
+ expect(() => {
+ instance.init(
+ createInitOptions({
+ instantSearchInstance,
+ parent: null,
+ })
+ );
+ }).not.toWarnDev(
+ '[InstantSearch.js]: The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.'
+ );
+ });
});
});
diff --git a/src/widgets/index/index.ts b/src/widgets/index/index.ts
index c74ca9fddf..2022793a8f 100644
--- a/src/widgets/index/index.ts
+++ b/src/widgets/index/index.ts
@@ -51,10 +51,14 @@ export type Index = Widget & {
getIndexId(): string;
getHelper(): Helper | null;
getResults(): SearchResults | null;
+ getScopedResults(): ScopedResult[];
getParent(): Index | null;
getWidgets(): Widget[];
+ createURL(state: SearchParameters): string;
+
addWidgets(widgets: Widget[]): Index;
removeWidgets(widgets: Widget[]): Index;
+
init(options: IndexInitOptions): void;
render(options: IndexRenderOptions): void;
dispose(): void;
@@ -103,7 +107,7 @@ function privateHelperSetState(
}
}
-function getLocalWidgetsState(
+function getLocalWidgetsUiState(
widgets: Widget[],
widgetStateOptions: WidgetUiStateOptions,
initialUiState: IndexUiState = {}
@@ -174,14 +178,6 @@ function resolveScopedResultsFromWidgets(widgets: Widget[]): ScopedResult[] {
}, []);
}
-function resolveScopedResultsFromIndex(widget: Index): ScopedResult[] {
- const widgetParent = widget.getParent();
- // If the widget is the root, we consider itself as the only sibling.
- const widgetSiblings = widgetParent ? widgetParent.getWidgets() : [widget];
-
- return resolveScopedResultsFromWidgets(widgetSiblings);
-}
-
const index = (props: IndexProps): Index => {
if (props === undefined || props.indexName === undefined) {
throw new Error(withUsage('The `indexName` option is required.'));
@@ -196,14 +192,6 @@ const index = (props: IndexProps): Index => {
let helper: Helper | null = null;
let derivedHelper: DerivedHelper | null = null;
- const createURL = (nextState: SearchParameters) =>
- localInstantSearchInstance!._createURL!({
- [indexId]: getLocalWidgetsState(localWidgets, {
- searchParameters: nextState,
- helper: helper!,
- }),
- });
-
return {
$$type: 'ais.index',
@@ -223,10 +211,28 @@ const index = (props: IndexProps): Index => {
return derivedHelper && derivedHelper.lastResults;
},
+ getScopedResults() {
+ const widgetParent = this.getParent();
+
+ // If the widget is the root, we consider itself as the only sibling.
+ const widgetSiblings = widgetParent ? widgetParent.getWidgets() : [this];
+
+ return resolveScopedResultsFromWidgets(widgetSiblings);
+ },
+
getParent() {
return localParent;
},
+ createURL(nextState: SearchParameters) {
+ return localInstantSearchInstance!._createURL!({
+ [indexId]: getLocalWidgetsUiState(localWidgets, {
+ searchParameters: nextState,
+ helper: helper!,
+ }),
+ });
+ },
+
getWidgets() {
return localWidgets;
},
@@ -278,7 +284,7 @@ const index = (props: IndexProps): Index => {
state: helper!.state,
renderState: localInstantSearchInstance!.renderState,
templatesConfig: localInstantSearchInstance!.templatesConfig,
- createURL,
+ createURL: this.createURL,
scopedResults: [],
searchMetadata: {
isSearchStalled: localInstantSearchInstance!._isSearchStalled,
@@ -304,7 +310,7 @@ const index = (props: IndexProps): Index => {
state: helper!.state,
renderState: localInstantSearchInstance!.renderState,
templatesConfig: localInstantSearchInstance!.templatesConfig,
- createURL,
+ createURL: this.createURL,
scopedResults: [],
searchMetadata: {
isSearchStalled: localInstantSearchInstance!._isSearchStalled,
@@ -344,7 +350,7 @@ const index = (props: IndexProps): Index => {
return next || state;
}, helper!.state);
- localUiState = getLocalWidgetsState(localWidgets, {
+ localUiState = getLocalWidgetsUiState(localWidgets, {
searchParameters: nextState,
helper: helper!,
});
@@ -484,7 +490,7 @@ const index = (props: IndexProps): Index => {
state: helper!.state,
renderState: instantSearchInstance.renderState,
templatesConfig: instantSearchInstance.templatesConfig,
- createURL,
+ createURL: this.createURL,
scopedResults: [],
searchMetadata: {
isSearchStalled: instantSearchInstance._isSearchStalled,
@@ -502,7 +508,9 @@ const index = (props: IndexProps): Index => {
localWidgets.forEach(widget => {
warning(
- !widget.getWidgetState,
+ // if it has NO getWidgetState or if it has getWidgetUiState, we don't warn
+ // aka we warn if there's _only_ getWidgetState
+ !widget.getWidgetState || Boolean(widget.getWidgetUiState),
'The `getWidgetState` method is renamed `getWidgetUiState` and will no longer exist under that name in InstantSearch.js 5.x. Please use `getWidgetUiState` instead.'
);
@@ -515,7 +523,7 @@ const index = (props: IndexProps): Index => {
state: helper!.state,
renderState: instantSearchInstance.renderState,
templatesConfig: instantSearchInstance.templatesConfig,
- createURL,
+ createURL: this.createURL,
scopedResults: [],
searchMetadata: {
isSearchStalled: instantSearchInstance._isSearchStalled,
@@ -536,7 +544,7 @@ const index = (props: IndexProps): Index => {
// @ts-ignore _uiState comes from privateHelperSetState and thus isn't typed on the helper event
const _uiState = event._uiState;
- localUiState = getLocalWidgetsState(
+ localUiState = getLocalWidgetsUiState(
localWidgets,
{
searchParameters: state,
@@ -567,11 +575,11 @@ const index = (props: IndexProps): Index => {
parent: this,
instantSearchInstance,
results: this.getResults()!,
- scopedResults: resolveScopedResultsFromIndex(this),
+ scopedResults: this.getScopedResults(),
state: this.getResults()!._state,
renderState: instantSearchInstance.renderState,
templatesConfig: instantSearchInstance.templatesConfig,
- createURL,
+ createURL: this.createURL,
searchMetadata: {
isSearchStalled: instantSearchInstance._isSearchStalled,
},
@@ -600,11 +608,11 @@ const index = (props: IndexProps): Index => {
parent: this,
instantSearchInstance,
results: this.getResults()!,
- scopedResults: resolveScopedResultsFromIndex(this),
+ scopedResults: this.getScopedResults(),
state: this.getResults()!._state,
renderState: instantSearchInstance.renderState,
templatesConfig: instantSearchInstance.templatesConfig,
- createURL,
+ createURL: this.createURL,
searchMetadata: {
isSearchStalled: instantSearchInstance._isSearchStalled,
},
@@ -665,7 +673,7 @@ const index = (props: IndexProps): Index => {
},
refreshUiState() {
- localUiState = getLocalWidgetsState(localWidgets, {
+ localUiState = getLocalWidgetsUiState(localWidgets, {
searchParameters: this.getHelper()!.state,
helper: this.getHelper()!,
});
diff --git a/stories/hits.stories.js b/stories/hits.stories.js
index 95a1627ab6..3e4517a967 100644
--- a/stories/hits.stories.js
+++ b/stories/hits.stories.js
@@ -58,6 +58,38 @@ storiesOf('Results/Hits', module)
]);
})
)
+ .add(
+ 'with reverseHighlight function',
+ withHits(({ search, container, instantsearch }) => {
+ search.addWidgets([
+ hits({
+ container,
+ templates: {
+ item(hit) {
+ return instantsearch.reverseHighlight({
+ attribute: 'name',
+ hit,
+ });
+ },
+ },
+ }),
+ ]);
+ })
+ )
+ .add(
+ 'with reverseHighlight helper',
+ withHits(({ search, container }) => {
+ search.addWidgets([
+ hits({
+ container,
+ templates: {
+ item:
+ '{{#helpers.reverseHighlight}}{ "attribute": "name" }{{/helpers.reverseHighlight}}',
+ },
+ }),
+ ]);
+ })
+ )
.add(
'with snippet function',
withHits(({ search, container, instantsearch }) => {
@@ -109,6 +141,57 @@ storiesOf('Results/Hits', module)
]);
})
)
+ .add(
+ 'with reverseSnippet function',
+ withHits(({ search, container, instantsearch }) => {
+ search.addWidgets([
+ configure({
+ attributesToSnippet: ['name', 'description'],
+ }),
+ ]);
+
+ search.addWidgets([
+ hits({
+ container,
+ templates: {
+ item(hit) {
+ return `
+ ${instantsearch.reverseSnippet({
+ attribute: 'name',
+ hit,
+ })}
+ ${instantsearch.reverseSnippet({
+ attribute: 'description',
+ hit,
+ })}
+ `;
+ },
+ },
+ }),
+ ]);
+ })
+ )
+ .add(
+ 'with reverseSnippet helper',
+ withHits(({ search, container }) => {
+ search.addWidgets([
+ configure({
+ attributesToSnippet: ['name', 'description'],
+ }),
+ ]);
+
+ search.addWidgets([
+ hits({
+ container,
+ templates: {
+ item: `
+ {{#helpers.reverseSnippet}}{ "attribute": "name", "highlightedTagName": "mark" }{{/helpers.reverseSnippet}}
+ {{#helpers.reverseSnippet}}{ "attribute": "description", "highlightedTagName": "mark" }{{/helpers.reverseSnippet}}
`,
+ },
+ }),
+ ]);
+ })
+ )
.add(
'with insights function',
withHits(