Reshaping sources
When you’re browsing a website that fetches content from a database, the UI isn’t fully representative of how that data is structured on the backend. This allows more human-friendly experiences and interactions. A search UI doesn’t have to be a one-to-one mapping with your search engine either.
The Autocomplete Reshape API lets you transform static, dynamic and asynchronous sources into a friendlier search UI.
Here are some examples of what you can do with the Reshape API:
- Apply a limit of items for a group of sources
- Remove duplicates in a group of sources
- Group sources by an attribute
- Sort sources
In this guide, you’ll learn how to use multiple reshape functions to remove duplicates and create a shared limit for your autocomplete suggestions.
Creating a reshape function
To remove duplicates between sources, you can start by creating a uniqBy
function. You can later apply this function to your Recent Searches and Query Suggestions sources.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function uniqBy(predicate) {
return function runUniqBy(...rawSources) {
const sources = rawSources.flat().filter(Boolean);
const seen = new Set();
return sources.map((source) => {
const items = source.getItems().filter((item) => {
const appliedItem = predicate({ source, item });
const hasSeen = seen.has(appliedItem);
seen.add(appliedItem);
return !hasSeen;
});
return {
...source,
getItems() {
return items;
},
};
});
};
}
Autocomplete supports conditional sources: sources sometimes exist and sometimes don’t.
This can happen when you display a source on an empty query but not when users start typing (for example, with popular queries).
Your function can support this by removing non-existent sources with filter(Boolean)
.
Using the reshape function
Now that you’ve created the uniqBy
function, you can specify the implementation for your sources and use it in the reshape
option:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const removeDuplicates = uniqBy(({ source, item }) =>
source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
);
autocomplete({
container: '#autocomplete',
plugins: [recentSearchesPlugin, querySuggestionsPlugin],
reshape({ sourcesBySourceId }) {
const {
recentSearchesPlugin,
querySuggestionsPlugin,
...rest
} = sourcesBySourceId;
return [
removeDuplicates(recentSearchesPlugin, querySuggestionsPlugin),
Object.values(rest),
];
},
});
The reshape option provides three arguments:
sources
: the resolved sources provided bygetSources
sourcesBySourceId
: the resolved sources grouped bysourceId
sstate
: the Autocomplete state
The uniqBy
function uses item.query
as identifier for the Query Suggestions plugin and item.label
for the Recent Searches plugin. These are the shape of the items that the plugins return. If you use custom sources, you can use a switch statement based on source.sourceId
.
Sources are retrieved from sourcesBySourceId
with their sourceId
using object destructuring.
You can return sources that you didn’t reshape with Object.values(rest)
.
Combining reshape functions
You can create a function that balances results from two different sources. It can be used with uniqBy
so that there are no duplicates and there’s a fixed number of combined items.
Notice how there are always four results showing, and that the number of Query Suggestions varies depending on the number of recent searches.
Here’s a limit
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function limit(value) {
return function runLimit(...rawSources) {
const sources = rawSources.flat().filter(Boolean);
const limitPerSource = Math.ceil(value / sources.length);
let sharedLimitRemaining = value;
return sources.map((source, index) => {
const isLastSource = index === sources.length - 1;
const sourceLimit = isLastSource
? sharedLimitRemaining
: Math.min(limitPerSource, sharedLimitRemaining);
const items = source.getItems().slice(0, sourceLimit);
sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0);
return {
...source,
getItems() {
return items;
},
};
});
};
}
You can then combine the uniqBy
and limit
functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const removeDuplicates = uniqBy(({ source, item }) =>
source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
);
const limitSuggestions = limit(4);
autocomplete({
container: '#autocomplete',
plugins: [recentSearchesPlugin, querySuggestionsPlugin],
reshape({ sourcesBySourceId }) {
const {
recentSearchesPlugin,
querySuggestionsPlugin,
...rest
} = sourcesBySourceId;
return [
limitSuggestions(
removeDuplicates(recentSearchesPlugin, querySuggestionsPlugin)
),
Object.values(rest),
];
},
});
Piping reshape functions
Nested function calls can become cumbersome, so you can use functional libraries like Ramda to pipe reshape functions instead.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { pipe } from 'ramda';
const combineSuggestions = pipe(
uniqBy(({ source, item }) =>
source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
),
limit(4)
);
autocomplete({
container: '#autocomplete',
plugins: [recentSearchesPlugin, querySuggestionsPlugin],
reshape({ sourcesBySourceId }) {
const {
recentSearchesPlugin,
querySuggestionsPlugin,
...rest
} = sourcesBySourceId;
return [
combineSuggestions(recentSearchesPlugin, querySuggestionsPlugin),
Object.values(rest),
];
},
});
For your reshape functions to support pipe
, you need to make sure your sources are one level deep, for example using Array.flat
.
You can find an example that groups products by category with the Reshape API in the sandboxes.