Skip to content

Commit

Permalink
feat: add recent-searches plugin (#316)
Browse files Browse the repository at this point in the history
* feat: add recent-searches plugin

* WIP

* fix rollup error (peerDeps)

* chore: exclude website from codesandbox ci builds

* expose getRecentSearches

* add getSources back

* build only esm for codesandbox ci

* add getSuggestions

* remove peerDependency

* renames

* rename methods

* resolve getSources from plugins too

* call onSubmit and onSelect for plugins

* update ci

* Apply suggestions from code review

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

* add key to distinguish multiple stores of recent searches

* early return

* named export

* apply feedbacks

* wrap plugin specific methods with data

* run onSubmit for plugins at getDefaultProps

* Update packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts

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

* add TData to plugins

* forward onSelect plugin calls in getDefaultProps

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

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

* rename payload to params

* add to store only when data exists

* use query as a fallback for queryID

* get input value from source

* fix type errors

Co-authored-by: François Chalifour <[email protected]>
  • Loading branch information
Eunjae Lee and francoischalifour authored Sep 24, 2020
1 parent 7783bc6 commit 858637e
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 22 deletions.
9 changes: 3 additions & 6 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
"packages": [
"packages/autocomplete-core",
"packages/autocomplete-preset-algolia",
"packages/autocomplete-js",
"packages/autocomplete-plugin-recent-searches"
],
"packages": ["packages/autocomplete-*"],
"buildCommand": false,
"^": "buildCommand is false because `yarn prepare` is going to build packages anyway.",
"sandboxes": ["github/algolia/autocomplete.js/tree/next/examples/js"]
}
47 changes: 44 additions & 3 deletions packages/autocomplete-core/src/getDefaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
generateAutocompleteId,
getItemsCount,
noop,
normalizeGetSources,
getNormalizedSources,
} from './utils';

export function getDefaultProps<TItem>(
Expand All @@ -12,6 +12,7 @@ export function getDefaultProps<TItem>(
const environment: typeof window = (typeof window !== 'undefined'
? window
: {}) as typeof window;
const plugins = props.plugins || [];

return {
debug: false,
Expand All @@ -24,7 +25,6 @@ export function getDefaultProps<TItem>(
environment,
shouldDropdownShow: ({ state }) => getItemsCount(state) > 0,
onStateChange: noop,
onSubmit: noop,
...props,
// Since `generateAutocompleteId` triggers a side effect (it increments
// and internal counter), we don't want to execute it if unnecessary.
Expand All @@ -41,7 +41,47 @@ export function getDefaultProps<TItem>(
context: {},
...props.initialState,
},
getSources: normalizeGetSources(props.getSources),
onSubmit: (params) => {
if (props.onSubmit) {
props.onSubmit(params);
}
plugins.forEach((plugin) => {
if (plugin.onSubmit) {
plugin.onSubmit(params);
}
});
},
getSources: (options) => {
const getSourcesFromPlugins = plugins
.map((plugin) => plugin.getSources)
.filter((getSources) => getSources !== undefined);

return Promise.all(
[...getSourcesFromPlugins, props.getSources].map((getSources) =>
getNormalizedSources(getSources!, options)
)
)
.then((nested) =>
// same as `nested.flat()`
nested.reduce((acc, array) => {
acc = acc.concat(array);
return acc;
}, [])
)
.then((sources) =>
sources.map((source) => ({
...source,
onSelect: (params) => {
source.onSelect(params);
plugins.forEach((plugin) => {
if (plugin.onSelect) {
plugin.onSelect(params);
}
});
},
}))
);
},
navigator: {
navigate({ suggestionUrl }) {
environment.location.assign(suggestionUrl);
Expand All @@ -62,5 +102,6 @@ export function getDefaultProps<TItem>(
},
...props.navigator,
},
plugins,
};
}
28 changes: 28 additions & 0 deletions packages/autocomplete-core/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,29 @@ interface Navigator<TItem> {
}): void;
}

export type AutocompletePlugin<TItem, TData> = {
/**
* The sources to get the suggestions from.
*/
getSources?(
params: GetSourcesParams<TItem>
):
| Array<AutocompleteSource<TItem>>
| Promise<Array<AutocompleteSource<TItem>>>;
/**
* The function called when the autocomplete form is submitted.
*/
onSubmit?(params: OnSubmitParams<TItem>): void;
/**
* Function called when an item is selected.
*/
onSelect?(params: OnSelectParams<TItem>): void;
/**
* An extra plugin specific object to store variables and functions
*/
data?: TData;
};

export interface AutocompleteOptions<TItem> {
/**
* Whether to consider the experience in debug mode.
Expand Down Expand Up @@ -247,6 +270,10 @@ export interface AutocompleteOptions<TItem> {
* updating the state.
*/
onInput?(params: OnInputParams<TItem>): void;
/**
* The array of plugins.
*/
plugins?: Array<AutocompletePlugin<TItem, unknown>>;
}

// Props manipulated internally with default values.
Expand All @@ -265,6 +292,7 @@ export interface InternalAutocompleteOptions<TItem>
getSources: GetSources<TItem>;
environment: Environment;
navigator: Navigator<TItem>;
plugins: Array<AutocompletePlugin<TItem, unknown>>;
shouldDropdownShow(params: { state: AutocompleteState<TItem> }): boolean;
onSubmit(params: OnSubmitParams<TItem>): void;
onInput?(params: OnInputParams<TItem>): void | Promise<any>;
Expand Down
24 changes: 11 additions & 13 deletions packages/autocomplete-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
InternalAutocompleteSource,
AutocompleteState,
AutocompleteSuggestion,
GetSources,
AutocompleteOptions,
AutocompleteSource,
} from './types';
Expand Down Expand Up @@ -57,18 +56,17 @@ function normalizeSource<TItem>(
};
}

export function normalizeGetSources<TItem>(
getSources: AutocompleteOptions<TItem>['getSources']
): GetSources<TItem> {
return (options) => {
return Promise.resolve(getSources(options)).then((sources) =>
Promise.all(
sources.filter(Boolean).map((source) => {
return Promise.resolve(normalizeSource<TItem>(source));
})
)
);
};
export function getNormalizedSources<TItem>(
getSources: AutocompleteOptions<TItem>['getSources'],
options
): Promise<Array<InternalAutocompleteSource<TItem>>> {
return Promise.resolve(getSources(options)).then((sources) =>
Promise.all(
sources.filter(Boolean).map((source) => {
return Promise.resolve(normalizeSource<TItem>(source));
})
)
);
}

export function getNextHighlightedIndex<TItem>(
Expand Down
11 changes: 11 additions & 0 deletions packages/autocomplete-plugin-recent-searches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @algolia/autocomplete-plugin-recent-searches

A plugin to add recent searches to Algolia Autocomplete.

## Installation

```sh
yarn add @algolia/autocomplete-plugin-recent-searches@alpha
# or
npm install @algolia/autocomplete-plugin-recent-searches@alpha
```
36 changes: 36 additions & 0 deletions packages/autocomplete-plugin-recent-searches/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@algolia/autocomplete-plugin-recent-searches",
"description": "A plugin to add recent searches to Algolia Autocomplete.",
"version": "1.0.0-alpha.28",
"license": "MIT",
"source": "src/index.ts",
"types": "dist/esm/index.d.ts",
"module": "dist/esm/index.js",
"main": "dist/umd/index.js",
"umd:main": "dist/umd/index.js",
"unpkg": "dist/umd/index.js",
"jsdelivr": "dist/umd/index.js",
"peerDependencies": {
"@algolia/autocomplete-core": "^1.0.0-alpha.28"
},
"scripts": {
"build:clean": "rm -rf ./dist",
"build:esm": "babel src --root-mode upward --extensions '.ts,.tsx' --out-dir dist/esm",
"build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/esm",
"build:umd": "rollup --config",
"build": "rm -rf ./dist && yarn build:umd && yarn build:esm && yarn build:types",
"on:change": "concurrently \"yarn build:esm\" \"yarn build:types\"",
"prepare": "yarn run build:esm",
"watch": "watch \"yarn on:change\" --ignoreDirectoryPattern \"/dist/\""
},
"homepage": "https://github.com/algolia/autocomplete.js",
"repository": "algolia/autocomplete.js",
"author": {
"name": "Algolia, Inc.",
"url": "https://www.algolia.com"
},
"sideEffects": false,
"files": [
"dist/"
]
}
17 changes: 17 additions & 0 deletions packages/autocomplete-plugin-recent-searches/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { plugins } from '../../rollup.base.config';
import { getBundleBanner } from '../../scripts/getBundleBanner';

import pkg from './package.json';

export default {
input: 'src/index.ts',
output: {
file: 'dist/umd/index.js',
format: 'umd',
sourcemap: true,
name: pkg.name,
banner: getBundleBanner(pkg),
},
external: ['@algolia/autocomplete-core'],
plugins,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AutocompletePlugin } from '@algolia/autocomplete-core';

import { createRecentSearchesStore } from './createRecentSearchesStore';

type PluginOptions = {
/**
* The number of searches to store.
*
* @default 5
*/
limit?: number;

/**
* The key to distinguish multiple stores of recent searches.
*
* @example
* // 'top_searchbar'
*/
key: string;
};

type RecentSearchItem = {
objectID: string;
query: string;
};

type PluginData = {
getFacetFilters: () => string[];
};

export function createRecentSearchesPlugin({
key,
limit = 5,
}: PluginOptions): AutocompletePlugin<RecentSearchItem, PluginData> {
const store = createRecentSearchesStore({
key: ['AUTOCOMPLETE_RECENT_SEARCHES', key].join('__'),
limit,
});

return {
getSources: ({ query }) => {
if (query) {
return [];
}

return [
{
getInputValue: ({ suggestion }) => suggestion.query,
getSuggestions() {
return store.getAll();
},
templates: {
item({ item }) {
return `
<div class="autocomplete-item">
<div>
<svg viewBox="0 0 22 22" width="16" height="16" fill="currentColor">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>
<span>${item.query}</span>
</div>
</div>
`;
},
},
},
];
},
onSubmit: ({ state }) => {
const { query } = state;
if (query) {
store.add({
objectID: query,
query,
});
}
},
onSelect: ({ suggestion, state, source }) => {
const inputValue = source.getInputValue({ suggestion, state });
const { objectID } = suggestion as any;
if (inputValue) {
store.add({
objectID: objectID || inputValue,
query: inputValue,
});
}
},
data: {
getFacetFilters: () => {
return store.getAll().map((item) => [`objectID:-${item.query}`]);
},
},
};
}
Loading

0 comments on commit 858637e

Please sign in to comment.