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

fix(getSuggestions): allow nested arrays to be returned #331

Merged
merged 14 commits into from
Oct 29, 2020
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import { createAutocomplete } from '..';
import { createAutocomplete, AutocompleteSuggestion } from '..';
import { InternalAutocompleteSource } from '../types';

function createSuggestion(items = []) {
function createSuggestion<TItem extends { label: string }>(
items: TItem[] = []
): AutocompleteSuggestion<TItem> {
return {
source: {
getInputValue: ({ suggestion }) => suggestion.label,
getSuggestionUrl: () => undefined,
onHighlight: () => {},
onSelect: () => {},
getSuggestions: () => items,
},
items,
};
}

interface AutocompleteMultiSuggestion<TItem> {
source: InternalAutocompleteSource<TItem>;
items: TItem[][];
}

function createMultiSuggestion<TItem extends { label: string }>(
items: TItem[][] = []
): AutocompleteMultiSuggestion<TItem> {
return {
source: {
getInputValue: ({ suggestion }) => suggestion.label,
Expand Down Expand Up @@ -57,22 +80,56 @@ describe('createAutocomplete', () => {
);
});

test('setSuggestions', () => {
const onStateChange = jest.fn();
const { setSuggestions } = createAutocomplete({
getSources: () => [],
onStateChange,
describe('setSuggestions', () => {
test('default', () => {
const onStateChange = jest.fn();
const { setSuggestions } = createAutocomplete({
getSources: () => [],
onStateChange,
});

setSuggestions([createSuggestion([{ label: 'hi' }])]);

expect(onStateChange).toHaveBeenCalledTimes(1);
expect(onStateChange).toHaveBeenCalledWith({
state: expect.objectContaining({
suggestions: [
{
items: [
{
label: 'hi',
__autocomplete_id: 0,
},
],
source: expect.any(Object),
},
],
}),
});
});
const suggestions = [createSuggestion()];

setSuggestions(suggestions);
test('flattens suggestions', () => {
const onStateChange = jest.fn();
const { setSuggestions } = createAutocomplete({
openOnFocus: true,
getSources: () => [],
onStateChange,
});

expect(onStateChange).toHaveBeenCalledTimes(1);
expect(onStateChange).toHaveBeenCalledWith(
expect.objectContaining({
state: expect.objectContaining({ suggestions }),
})
);
// while it also accepts a nested array.
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
// There's only one type for both input and output however,
setSuggestions([createMultiSuggestion([[{ label: 'hi' }]])]);
Haroenv marked this conversation as resolved.
Show resolved Hide resolved

expect(onStateChange).toHaveBeenCalledWith({
state: expect.objectContaining({
suggestions: [
expect.objectContaining({
items: [{ label: 'hi', __autocomplete_id: 0 }],
}),
],
}),
});
});
});

test('setIsOpen', () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/autocomplete-core/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { flatten } from '../utils';

describe('flatten', () => {
it('does not split strings', () => {
expect(flatten(['value', 'value'])).toEqual(['value', 'value']);
});

it('spreads arrays', () => {
expect(flatten(['value', ['value']])).toEqual(['value', 'value']);
});

it('ignores empty arrays', () => {
expect(flatten([[], 'value', 'value'])).toEqual(['value', 'value']);
});
});
5 changes: 4 additions & 1 deletion packages/autocomplete-core/src/getAutocompleteSetters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AutocompleteApi, AutocompleteStore } from './types';
import { flatten } from './utils';

interface GetAutocompleteSettersOptions<TItem> {
store: AutocompleteStore<TItem>;
Expand All @@ -23,7 +24,9 @@ export function getAutocompleteSetters<TItem>({
let baseItemId = 0;
const value = rawValue.map((suggestion) => ({
...suggestion,
items: suggestion.items.map((item) => ({
// We flatten the stored items to support calling `getAlgoliaHits`
// from the source itself.
items: flatten(suggestion.items).map((item) => ({
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
...item,
__autocomplete_id: baseItemId++,
})),
Expand Down
7 changes: 2 additions & 5 deletions packages/autocomplete-core/src/getDefaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getItemsCount,
noop,
getNormalizedSources,
flatten,
} from './utils';

export function getDefaultProps<TItem>(
Expand Down Expand Up @@ -62,11 +63,7 @@ export function getDefaultProps<TItem>(
)
)
.then((nested) =>
// same as `nested.flat()`
nested.reduce((acc, array) => {
acc = acc.concat(array);
return acc;
}, [])
flatten(nested)
)
.then((sources) =>
sources.map((source) => ({
Expand Down
4 changes: 3 additions & 1 deletion packages/autocomplete-core/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export interface AutocompleteSource<TItem> {
/**
* Function called when the input changes. You can use this function to filter/search the items based on the query.
*/
getSuggestions(params: GetSourcesParams<TItem>): MaybePromise<TItem[]>;
getSuggestions(
params: GetSourcesParams<TItem>
): MaybePromise<TItem[] | TItem[][]>;
/**
* Function called when an item is selected.
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/autocomplete-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,9 @@ export function getHighlightedItem<TItem>({
export function isOrContainsNode(parent: Node, child: Node) {
return parent === child || (parent.contains && parent.contains(child));
}

export function flatten<TType>(values: Array<TType | TType[]>): TType[] {
return values.reduce<TType[]>((a, b) => {
return a.concat(b);
}, []);
}