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

feat(core): introduce Reshape API #647

Merged
merged 10 commits into from
Aug 26, 2021
Merged
2 changes: 1 addition & 1 deletion .codesandbox/ci.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that it's not needed to have pipe builtin


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 combineSuggestions = pipe(
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
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],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is products a plugin and not a regular source? could indicate more "regular" usage?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gives me the idea if longer term plugins and sources maybe could be merged into a single argument? plugins could have no source, and it would allow you to order before using reshape already

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is products a plugin and not a regular source? could indicate more "regular" usage?

I wanted to focus the example on the reshape API, and not pollute the file with a huge source.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gives me the idea if longer term plugins and sources maybe could be merged into a single argument? plugins could have no source, and it would allow you to order before using reshape already

You mean something like this?

autocomplete({
  // ...
  getSources() {
    return [
      recentSearchesPlugin,
      querySuggestionsPlugin,
      {
        // custom source
      }
    ]
  }
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more thinking of plugins: [somePlugin, { getSources() { customStuff }}, maybe that way you don't really need the callback form (although that limits some use cases for conditional requests, in which case your suggestion fits better)

I was just wondering if we really need to distinguish between the two

reshape({ sourcesBySourceId }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reasonable name!

const {
recentSearchesPlugin,
querySuggestionsPlugin,
products,
...rest
} = sourcesBySourceId;

return [
combineSuggestions(recentSearchesPlugin, querySuggestionsPlugin),
groupByCategory(products),
Object.values(rest),
];
},
});
10 changes: 10 additions & 0 deletions examples/reshape/env.ts
Original file line number Diff line number Diff line change
@@ -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
Loading
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
AutocompleteReshapeSource,
BaseItem,
} from '@algolia/autocomplete-core';

export type AutocompleteReshapeFunction<TParams = any> = <
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
TItem extends BaseItem
>(
...params: TParams[]
) => (
...expressions: Array<AutocompleteReshapeSource<TItem>>
) => Array<AutocompleteReshapeSource<TItem>>;
68 changes: 68 additions & 0 deletions examples/reshape/functions/groupBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 = <
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we've pulled Ramda already in this example, I'd use it there instead of rolling our own groupBy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}, {});

const groupNames = Object.keys(groupedItems);

return groupNames.map((groupName) => {
const groupItems = groupedItems[groupName];
shortcuts marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './groupBy';
export * from './limit';
export * from './uniqBy';
30 changes: 30 additions & 0 deletions examples/reshape/functions/limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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 items = source
.getItems()
.slice(
0,
isLastSource
? sharedLimitRemaining
: Math.min(limitPerSource, sharedLimitRemaining)
sarahdayan marked this conversation as resolved.
Show resolved Hide resolved
);
sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0);

return {
...source,
getItems() {
return items;
},
};
});
};
};
13 changes: 13 additions & 0 deletions examples/reshape/functions/normalizeReshapeSources.ts
Original file line number Diff line number Diff line change
@@ -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>(
sarahdayan marked this conversation as resolved.
Show resolved Hide resolved
sources: Array<AutocompleteReshapeSource<TItem>>
) {
return flatten(sources).filter(Boolean);
}
41 changes: 41 additions & 0 deletions examples/reshape/functions/uniqBy.ts
Original file line number Diff line number Diff line change
@@ -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>> = <
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we've pulled Ramda already in this example, I'd use it there instead of rolling our own uniqBy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of using ramda in the examples, I don't think it's so common, and might cause people to include it just because our example does so

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, it will not make it possible for them to copy/paste.

Copy link
Member

@sarahdayan sarahdayan Aug 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@francoischalifour You recommended that I go with Ramda's groupBy in the Tags plugin example though :) Also, we do use Ramda in the example for pipe. Why do you think this is okay for this specific function?

Anyway it's no biggie and not blocking. We can discuss that later.

TItem extends BaseItem
>(
predicate
) => {
return function runUniqBy(...rawSources) {
const sources = normalizeReshapeSources(rawSources);
const seen: TItem[] = [];

return sources.map((source) => {
const items = source.getItems().filter((item) => {
const appliedItem = predicate({ source, item });
const hasSeen = seen.includes(appliedItem);
Haroenv marked this conversation as resolved.
Show resolved Hide resolved

seen.push(appliedItem);

return !hasSeen;
});

return {
...source,
getItems() {
return items;
},
};
});
};
};
20 changes: 20 additions & 0 deletions examples/reshape/index.html
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
]
}
Loading