Skip to content

Commit

Permalink
feat(core): introduce Reshape API (#647)
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Aug 26, 2021
1 parent 0a70513 commit d6180d2
Show file tree
Hide file tree
Showing 33 changed files with 951 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .codesandbox/ci.json
Expand Up @@ -10,7 +10,7 @@
"/examples/react-renderer",
"/examples/starter-algolia",
"/examples/starter",
"/examples/voice-search",
"/examples/reshape",
"/examples/vue"
],
"node": "14"
Expand Down
Empty file added examples/reshape/README.md
Empty file.
74 changes: 74 additions & 0 deletions examples/reshape/app.tsx
@@ -0,0 +1,74 @@
/** @jsx h */
import { autocomplete } from '@algolia/autocomplete-js';
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
import { h, Fragment } from 'preact';
import { pipe } from 'ramda';

import '@algolia/autocomplete-theme-classic';

import { groupBy, limit, uniqBy } from './functions';
import { productsPlugin } from './productsPlugin';
import { searchClient } from './searchClient';

const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: 'search',
limit: 10,
});
const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
indexName: 'instant_search_demo_query_suggestions',
getSearchParams() {
return {
hitsPerPage: 10,
};
},
});

const dedupeAndLimitSuggestions = pipe(
uniqBy(({ source, item }) =>
source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label
),
limit(4)
);
const groupByCategory = groupBy((hit) => hit.categories[0], {
getSource({ name, items }) {
return {
getItems() {
return items.slice(0, 3);
},
templates: {
header() {
return (
<Fragment>
<span className="aa-SourceHeaderTitle">{name}</span>
<div className="aa-SourceHeaderLine" />
</Fragment>
);
},
},
};
},
});

autocomplete({
container: '#autocomplete',
placeholder: 'Search',
debug: true,
openOnFocus: true,
plugins: [recentSearchesPlugin, querySuggestionsPlugin, productsPlugin],
reshape({ sourcesBySourceId }) {
const {
recentSearchesPlugin,
querySuggestionsPlugin,
products,
...rest
} = sourcesBySourceId;

return [
dedupeAndLimitSuggestions(recentSearchesPlugin, querySuggestionsPlugin),
groupByCategory(products),
Object.values(rest),
];
},
});
10 changes: 10 additions & 0 deletions examples/reshape/env.ts
@@ -0,0 +1,10 @@
import * as preact from 'preact';

// Parcel picks the `source` field of the monorepo packages and thus doesn't
// apply the Babel config. We therefore need to manually override the constants
// in the app, as well as the React pragmas.
// See https://twitter.com/devongovett/status/1134231234605830144
(global as any).__DEV__ = process.env.NODE_ENV !== 'production';
(global as any).__TEST__ = false;
(global as any).h = preact.h;
(global as any).React = preact;
Binary file added examples/reshape/favicon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/reshape/functions/AutocompleteReshapeFunction.ts
@@ -0,0 +1,12 @@
import {
AutocompleteReshapeSource,
BaseItem,
} from '@algolia/autocomplete-core';

export type AutocompleteReshapeFunction<TParams = any> = <
TItem extends BaseItem
>(
...params: TParams[]
) => (
...expressions: Array<AutocompleteReshapeSource<TItem>>
) => Array<AutocompleteReshapeSource<TItem>>;
65 changes: 65 additions & 0 deletions examples/reshape/functions/groupBy.ts
@@ -0,0 +1,65 @@
import { BaseItem } from '@algolia/autocomplete-core';
import { AutocompleteSource } from '@algolia/autocomplete-js';
import { flatten } from '@algolia/autocomplete-shared';

import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction';
import { normalizeReshapeSources } from './normalizeReshapeSources';

export type GroupByOptions<
TItem extends BaseItem,
TSource extends AutocompleteSource<TItem>
> = {
getSource(params: { name: string; items: TItem[] }): Partial<TSource>;
};

export const groupBy: AutocompleteReshapeFunction = <
TItem extends BaseItem,
TSource extends AutocompleteSource<TItem> = AutocompleteSource<TItem>
>(
predicate: (value: TItem) => string,
options: GroupByOptions<TItem, TSource>
) => {
return function runGroupBy(...rawSources) {
const sources = normalizeReshapeSources(rawSources);

if (sources.length === 0) {
return [];
}

// Since we create multiple sources from a single one, we take the first one
// as reference to create the new sources from.
const referenceSource = sources[0];
const items = flatten(sources.map((source) => source.getItems()));
const groupedItems = items.reduce<Record<string, TItem[]>>((acc, item) => {
const key = predicate(item as TItem);

if (!acc.hasOwnProperty(key)) {
acc[key] = [];
}

acc[key].push(item as TItem);

return acc;
}, {});

return Object.entries(groupedItems).map(([groupName, groupItems]) => {
const userSource = options.getSource({
name: groupName,
items: groupItems,
});

return {
...referenceSource,
sourceId: groupName,
getItems() {
return groupItems;
},
...userSource,
templates: {
...((referenceSource as any).templates as any),
...(userSource as any).templates,
},
};
});
};
};
3 changes: 3 additions & 0 deletions examples/reshape/functions/index.ts
@@ -0,0 +1,3 @@
export * from './groupBy';
export * from './limit';
export * from './uniqBy';
26 changes: 26 additions & 0 deletions examples/reshape/functions/limit.ts
@@ -0,0 +1,26 @@
import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction';
import { normalizeReshapeSources } from './normalizeReshapeSources';

export const limit: AutocompleteReshapeFunction<number> = (value) => {
return function runLimit(...rawSources) {
const sources = normalizeReshapeSources(rawSources);
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;
},
};
});
};
};
13 changes: 13 additions & 0 deletions examples/reshape/functions/normalizeReshapeSources.ts
@@ -0,0 +1,13 @@
import {
AutocompleteReshapeSource,
BaseItem,
} from '@algolia/autocomplete-core';
import { flatten } from '@algolia/autocomplete-shared';

// We filter out falsy values because dynamic sources may not exist at every render.
// We flatten to support pipe operators from functional libraries like Ramda.
export function normalizeReshapeSources<TItem extends BaseItem>(
sources: Array<AutocompleteReshapeSource<TItem>>
) {
return flatten(sources).filter(Boolean);
}
41 changes: 41 additions & 0 deletions examples/reshape/functions/uniqBy.ts
@@ -0,0 +1,41 @@
import {
AutocompleteReshapeSource,
BaseItem,
} from '@algolia/autocomplete-core';

import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction';
import { normalizeReshapeSources } from './normalizeReshapeSources';

type UniqByPredicate<TItem extends BaseItem> = (params: {
source: AutocompleteReshapeSource<TItem>;
item: TItem;
}) => TItem;

export const uniqBy: AutocompleteReshapeFunction<UniqByPredicate<any>> = <
TItem extends BaseItem
>(
predicate
) => {
return function runUniqBy(...rawSources) {
const sources = normalizeReshapeSources(rawSources);
const seen = new Set<TItem>();

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;
},
};
});
};
};
20 changes: 20 additions & 0 deletions examples/reshape/index.html
@@ -0,0 +1,20 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
<link rel="stylesheet" href="style.css" />

<title>Reshape API | Autocomplete</title>
</head>

<body>
<div class="container">
<div id="autocomplete"></div>
</div>

<script src="env.ts"></script>
<script src="app.tsx"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions examples/reshape/package.json
@@ -0,0 +1,33 @@
{
"name": "@algolia/autocomplete-example-reshape",
"description": "Autocomplete example with the Reshape API",
"version": "1.2.2",
"private": true,
"license": "MIT",
"scripts": {
"build": "parcel build index.html",
"start": "parcel index.html"
},
"dependencies": {
"@algolia/autocomplete-js": "1.2.2",
"@algolia/autocomplete-plugin-query-suggestions": "1.2.2",
"@algolia/autocomplete-plugin-recent-searches": "1.2.2",
"@algolia/autocomplete-preset-algolia": "1.2.2",
"@algolia/autocomplete-shared": "1.2.2",
"@algolia/autocomplete-theme-classic": "1.2.2",
"@algolia/client-search": "4.9.1",
"algoliasearch": "4.9.1",
"preact": "10.5.13",
"ramda": "0.27.1",
"search-insights": "1.7.1"
},
"devDependencies": {
"@algolia/autocomplete-core": "1.2.2",
"parcel": "2.0.0-beta.2"
},
"keywords": [
"algolia",
"autocomplete",
"javascript"
]
}

0 comments on commit d6180d2

Please sign in to comment.