Skip to content

Commit

Permalink
fix(getSuggestions): allow nested arrays to be returned (#331)
Browse files Browse the repository at this point in the history
* fix(getSuggestions): allow nested arrays to be returned

this is useful, since it allows getAlgoliaHits to be used top-level, but also within a single source, without having to take the first element of the array manually

* Update packages/autocomplete-core/src/getAutocompleteSetters.ts

Co-authored-by: François Chalifour <[email protected]>

* inline logic in test

* better wording for test

* refactor test

* revert public api change

* idk

* fix import in test

* change type in test

* remove comment

* fix tests based on rebase

* fix type

* remove multi

* remove more

Co-authored-by: François Chalifour <[email protected]>
  • Loading branch information
Haroenv and francoischalifour authored Oct 29, 2020
1 parent d39f4cf commit 753c8ca
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createAutocomplete } from '..';
import { createAutocomplete, AutocompleteSuggestion } from '..';

function createSuggestion(items = []) {
function createSuggestion<TItem extends { label: string }>(
items: TItem[] | TItem[][] = []
): AutocompleteSuggestion<TItem | TItem[]> | AutocompleteSuggestion<TItem[]> {
return {
source: {
getInputValue: ({ suggestion }) => suggestion.label,
Expand Down Expand Up @@ -57,22 +59,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({
prevState: expect.any(Object),
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 }),
})
);
setSuggestions([createSuggestion([[{ label: 'hi' }]])]);

expect(onStateChange).toHaveBeenCalledWith({
prevState: expect.any(Object),
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) => ({
...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);
}, []);
}

0 comments on commit 753c8ca

Please sign in to comment.