Skip to content

Commit

Permalink
feat(voice): add additional query parameters (#3738)
Browse files Browse the repository at this point in the history
Two new arguments:
- `additionalQueryParameters: () => Partial<SearchParameters>` (only applied when voice search is mounted)
- `language: string` (iso 639-1), the parameter sent to Algolia will be always the short version

known limitation: additional query parameters will stay applied as long as the Voice Search widget is mounted, meaning they can cause stale values if you switch input method without unmounting (limitation of the architecture of InstantSearch)

Co-Authored-By: haroenv <haroen@algolia.com>
  • Loading branch information
marieglr and Haroenv committed Oct 23, 2019
1 parent 25716fc commit c555255
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 7 deletions.
115 changes: 112 additions & 3 deletions src/connectors/voice-search/__tests__/connectVoiceSearch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jest.mock('../../../lib/voiceSearchHelper', () => {
};
});

function getInitializedWidget() {
function getInitializedWidget({ widgetParams = {} } = {}) {
const helper = algoliasearchHelper({}, '');

const renderFn = () => {};
const makeWidget = connectVoiceSearch(renderFn);
const widget = makeWidget({});
const widget = makeWidget(widgetParams);

helper.search = () => {};
widget.init({ helper });
Expand All @@ -30,7 +30,7 @@ function getInitializedWidget() {
renderFn,
widget,
helper,
refine: query => widget._refine(query),
refine: widget._refine,
};
}

Expand Down Expand Up @@ -277,4 +277,113 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/voice-searc
);
});
});

describe('additional search parameters', () => {
it('applies default search parameters if given', () => {
const { helper, refine } = getInitializedWidget({
widgetParams: {
additionalQueryParameters: () => {},
},
});

refine('query');
expect(helper.state).toEqual(
new SearchParameters({
ignorePlurals: true,
removeStopWords: true,
optionalWords: 'query',
queryLanguages: undefined,
index: '',
query: 'query',
})
);
});

it('applies queryLanguages if language given', () => {
const { helper, refine } = getInitializedWidget({
widgetParams: {
language: 'en-US',
additionalQueryParameters: () => {},
},
});

refine('query');
expect(helper.state).toEqual(
new SearchParameters({
queryLanguages: ['en'],
// regular
removeStopWords: true,
optionalWords: 'query',
ignorePlurals: true,
query: 'query',
index: '',
})
);
});

it('applies additional parameters if language given', () => {
const { helper, refine } = getInitializedWidget({
widgetParams: {
additionalQueryParameters: () => ({
distinct: true,
}),
},
});

refine('query');
expect(helper.state).toEqual(
new SearchParameters({
ignorePlurals: true,
removeStopWords: true,
optionalWords: 'query',
queryLanguages: undefined,
index: '',
query: 'query',
distinct: true,
})
);
});

it('removes additional parameters when disposed', () => {
const { widget, helper, refine } = getInitializedWidget({
widgetParams: {
additionalQueryParameters: () => {},
},
});

refine('query');
const newState = widget.dispose({ state: helper.state });
expect(newState).toEqual(
new SearchParameters({
ignorePlurals: undefined,
removeStopWords: undefined,
optionalWords: undefined,
queryLanguages: undefined,
index: '',
})
);
});

it('removes additional parameters and extra parameters when disposed', () => {
const { widget, helper, refine } = getInitializedWidget({
widgetParams: {
additionalQueryParameters: () => ({
distinct: true,
}),
},
});

refine('query');
const newState = widget.dispose({ state: helper.state });
expect(newState).toEqual(
new SearchParameters({
ignorePlurals: undefined,
removeStopWords: undefined,
optionalWords: undefined,
queryLanguages: undefined,
index: '',
})
);
});
});
});
52 changes: 49 additions & 3 deletions src/connectors/voice-search/connectVoiceSearch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PlainSearchParameters } from 'algoliasearch-helper';
import {
checkRendering,
createDocumentationMessageGenerator,
Expand All @@ -15,7 +16,11 @@ const withUsage = createDocumentationMessageGenerator({
});

export type VoiceSearchConnectorParams = {
searchAsYouSpeak?: boolean;
searchAsYouSpeak: boolean;
language?: string;
additionalQueryParameters?: (params: {
query: string;
}) => PlainSearchParameters | void;
};

export interface VoiceSearchRendererOptions<TVoiceSearchWidgetParams>
Expand Down Expand Up @@ -71,20 +76,42 @@ const connectVoiceSearch: VoiceSearchConnector = (
);
};

const { searchAsYouSpeak = false } = widgetParams;
const {
searchAsYouSpeak = false,
language,
additionalQueryParameters,
} = widgetParams;

return {
$$type: 'ais.voiceSearch',

init({ helper, instantSearchInstance }) {
(this as any)._refine = (query: string): void => {
if (query !== helper.state.query) {
const queryLanguages = language
? [language.split('-')[0]]
: undefined;
helper.setQueryParameter('queryLanguages', queryLanguages);

if (typeof additionalQueryParameters === 'function') {
helper.setState(
helper.state.setQueryParameters({
ignorePlurals: true,
removeStopWords: true,
// @ts-ignore (optionalWords only allows array, while string is also valid)
optionalWords: query,
...additionalQueryParameters({ query }),
})
);
}

helper.setQuery(query).search();
}
};

(this as any)._voiceSearchHelper = createVoiceSearchHelper({
searchAsYouSpeak,
language,
onQueryChange: query => (this as any)._refine(query),
onStateChange: () => {
render({
Expand Down Expand Up @@ -115,7 +142,26 @@ const connectVoiceSearch: VoiceSearchConnector = (

unmountFn();

return state.setQueryParameter('query', undefined);
let newState = state;
if (typeof additionalQueryParameters === 'function') {
const additional = additionalQueryParameters({ query: '' });
const toReset = additional
? Object.keys(additional).reduce((acc, current) => {
acc[current] = undefined;
return acc;
}, {})
: {};
newState = state.setQueryParameters({
// @ts-ignore (queryLanguages is not yet added to algoliasearch)
queryLanguages: undefined,
ignorePlurals: undefined,
removeStopWords: undefined,
optionalWords: undefined,
...toReset,
});
}

return newState.setQueryParameter('query', undefined);
},

getWidgetState(uiState, { searchParameters }) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/voiceSearchHelper/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type VoiceSearchHelperParams = {
searchAsYouSpeak: boolean;
language?: string;
onQueryChange: (query: string) => void;
onStateChange: () => void;
};
Expand Down Expand Up @@ -31,6 +32,7 @@ export type ToggleListening = () => void;

export default function createVoiceSearchHelper({
searchAsYouSpeak,
language,
onQueryChange,
onStateChange,
}: VoiceSearchHelperParams): VoiceSearchHelper {
Expand Down Expand Up @@ -105,6 +107,11 @@ export default function createVoiceSearchHelper({
}
resetState('askingPermission');
recognition.interimResults = true;

if (language) {
recognition.lang = language;
}

recognition.addEventListener('start', onStart);
recognition.addEventListener('error', onError);
recognition.addEventListener('result', onResult);
Expand Down
11 changes: 10 additions & 1 deletion src/widgets/voice-search/voice-search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { render, unmountComponentAtNode } from 'preact-compat';
import cx from 'classnames';
import { PlainSearchParameters } from 'algoliasearch-helper';
import {
getContainerNode,
createDocumentationMessageGenerator,
Expand Down Expand Up @@ -42,6 +43,10 @@ type VoiceSearchWidgetParams = {
cssClasses?: Partial<VoiceSearchCSSClasses>;
templates?: Partial<VoiceSearchTemplates>;
searchAsYouSpeak?: boolean;
language?: string;
additionalQueryParameters?: (params: {
query: string;
}) => PlainSearchParameters | void;
};

interface VoiceSearchRendererWidgetParams extends VoiceSearchWidgetParams {
Expand Down Expand Up @@ -79,7 +84,9 @@ const voiceSearch: VoiceSearch = (
container,
cssClasses: userCssClasses = {} as VoiceSearchCSSClasses,
templates,
searchAsYouSpeak,
searchAsYouSpeak = false,
language,
additionalQueryParameters,
} = {} as VoiceSearchWidgetParams
) => {
if (!container) {
Expand All @@ -103,6 +110,8 @@ const voiceSearch: VoiceSearch = (
cssClasses,
templates: { ...defaultTemplates, ...templates },
searchAsYouSpeak,
language,
additionalQueryParameters,
});
};

Expand Down
20 changes: 20 additions & 0 deletions stories/voice-search.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,24 @@ storiesOf('VoiceSearch', module)
})
);
})
)
.add(
'with additional parameters',
withHits(({ search, container }) => {
const descContainer = document.createElement('div');
const realContainer = document.createElement('div');
container.appendChild(descContainer);
container.appendChild(realContainer);
descContainer.innerHTML = `
<p>Sets the default additional parameters, as well as a language</p>
`;

search.addWidget(
voiceSearch({
container: realContainer,
language: 'en-US',
additionalQueryParameters: () => {},
})
);
})
);

0 comments on commit c555255

Please sign in to comment.