From 858637e34ba5bfcdfa8bf66e8785296afd436971 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Thu, 24 Sep 2020 11:30:34 +0200 Subject: [PATCH] feat: add recent-searches plugin (#316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * add TData to plugins * forward onSelect plugin calls in getDefaultProps * Update packages/autocomplete-core/src/getDefaultProps.ts Co-authored-by: François Chalifour * 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 --- .codesandbox/ci.json | 9 +- .../autocomplete-core/src/getDefaultProps.ts | 47 ++++++++- packages/autocomplete-core/src/types/api.ts | 28 ++++++ packages/autocomplete-core/src/utils.ts | 24 +++-- .../README.md | 11 +++ .../package.json | 36 +++++++ .../rollup.config.js | 17 ++++ .../src/createRecentSearchesPlugin.ts | 96 +++++++++++++++++++ .../src/createRecentSearchesStore.ts | 64 +++++++++++++ .../src/index.ts | 1 + .../tsconfig.declaration.json | 3 + 11 files changed, 314 insertions(+), 22 deletions(-) create mode 100644 packages/autocomplete-plugin-recent-searches/README.md create mode 100644 packages/autocomplete-plugin-recent-searches/package.json create mode 100644 packages/autocomplete-plugin-recent-searches/rollup.config.js create mode 100644 packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts create mode 100644 packages/autocomplete-plugin-recent-searches/src/index.ts create mode 100644 packages/autocomplete-plugin-recent-searches/tsconfig.declaration.json diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index d6eeb0ce3..0dd63b92b 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -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"] } diff --git a/packages/autocomplete-core/src/getDefaultProps.ts b/packages/autocomplete-core/src/getDefaultProps.ts index 920095042..5d73722d4 100644 --- a/packages/autocomplete-core/src/getDefaultProps.ts +++ b/packages/autocomplete-core/src/getDefaultProps.ts @@ -3,7 +3,7 @@ import { generateAutocompleteId, getItemsCount, noop, - normalizeGetSources, + getNormalizedSources, } from './utils'; export function getDefaultProps( @@ -12,6 +12,7 @@ export function getDefaultProps( const environment: typeof window = (typeof window !== 'undefined' ? window : {}) as typeof window; + const plugins = props.plugins || []; return { debug: false, @@ -24,7 +25,6 @@ export function getDefaultProps( 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. @@ -41,7 +41,47 @@ export function getDefaultProps( 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); @@ -62,5 +102,6 @@ export function getDefaultProps( }, ...props.navigator, }, + plugins, }; } diff --git a/packages/autocomplete-core/src/types/api.ts b/packages/autocomplete-core/src/types/api.ts index a7b4f035a..4dbf86804 100644 --- a/packages/autocomplete-core/src/types/api.ts +++ b/packages/autocomplete-core/src/types/api.ts @@ -153,6 +153,29 @@ interface Navigator { }): void; } +export type AutocompletePlugin = { + /** + * The sources to get the suggestions from. + */ + getSources?( + params: GetSourcesParams + ): + | Array> + | Promise>>; + /** + * The function called when the autocomplete form is submitted. + */ + onSubmit?(params: OnSubmitParams): void; + /** + * Function called when an item is selected. + */ + onSelect?(params: OnSelectParams): void; + /** + * An extra plugin specific object to store variables and functions + */ + data?: TData; +}; + export interface AutocompleteOptions { /** * Whether to consider the experience in debug mode. @@ -247,6 +270,10 @@ export interface AutocompleteOptions { * updating the state. */ onInput?(params: OnInputParams): void; + /** + * The array of plugins. + */ + plugins?: Array>; } // Props manipulated internally with default values. @@ -265,6 +292,7 @@ export interface InternalAutocompleteOptions getSources: GetSources; environment: Environment; navigator: Navigator; + plugins: Array>; shouldDropdownShow(params: { state: AutocompleteState }): boolean; onSubmit(params: OnSubmitParams): void; onInput?(params: OnInputParams): void | Promise; diff --git a/packages/autocomplete-core/src/utils.ts b/packages/autocomplete-core/src/utils.ts index 96513f73e..28f6aafe4 100644 --- a/packages/autocomplete-core/src/utils.ts +++ b/packages/autocomplete-core/src/utils.ts @@ -3,7 +3,6 @@ import { InternalAutocompleteSource, AutocompleteState, AutocompleteSuggestion, - GetSources, AutocompleteOptions, AutocompleteSource, } from './types'; @@ -57,18 +56,17 @@ function normalizeSource( }; } -export function normalizeGetSources( - getSources: AutocompleteOptions['getSources'] -): GetSources { - return (options) => { - return Promise.resolve(getSources(options)).then((sources) => - Promise.all( - sources.filter(Boolean).map((source) => { - return Promise.resolve(normalizeSource(source)); - }) - ) - ); - }; +export function getNormalizedSources( + getSources: AutocompleteOptions['getSources'], + options +): Promise>> { + return Promise.resolve(getSources(options)).then((sources) => + Promise.all( + sources.filter(Boolean).map((source) => { + return Promise.resolve(normalizeSource(source)); + }) + ) + ); } export function getNextHighlightedIndex( diff --git a/packages/autocomplete-plugin-recent-searches/README.md b/packages/autocomplete-plugin-recent-searches/README.md new file mode 100644 index 000000000..44df4ec92 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/README.md @@ -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 +``` diff --git a/packages/autocomplete-plugin-recent-searches/package.json b/packages/autocomplete-plugin-recent-searches/package.json new file mode 100644 index 000000000..cfc6ab444 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/package.json @@ -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/" + ] +} diff --git a/packages/autocomplete-plugin-recent-searches/rollup.config.js b/packages/autocomplete-plugin-recent-searches/rollup.config.js new file mode 100644 index 000000000..be58b7a35 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/rollup.config.js @@ -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, +}; diff --git a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts new file mode 100644 index 000000000..5aa93c873 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts @@ -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 { + 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 ` +
+
+ + + + + + ${item.query} +
+
+ `; + }, + }, + }, + ]; + }, + 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}`]); + }, + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts new file mode 100644 index 000000000..8a31cfce3 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesStore.ts @@ -0,0 +1,64 @@ +function isLocalStorageSupported() { + const key = '__TEST_KEY__'; + + try { + localStorage.setItem(key, ''); + localStorage.removeItem(key); + + return true; + } catch (error) { + return false; + } +} + +function createStorage(key) { + if (isLocalStorageSupported() === false) { + return { + setItem() {}, + getItem() { + return []; + }, + }; + } + + return { + setItem(item) { + return window.localStorage.setItem(key, JSON.stringify(item)); + }, + getItem() { + const item = window.localStorage.getItem(key); + + return item ? JSON.parse(item) : []; + }, + }; +} + +export function createRecentSearchesStore({ key, limit }) { + const storage = createStorage(key); + let items = storage.getItem().slice(0, limit); + + return { + add(item) { + const isQueryAlreadySaved = items.findIndex( + (x) => x.objectID === item.objectID + ); + + if (isQueryAlreadySaved > -1) { + items.splice(isQueryAlreadySaved, 1); + } + + items.unshift(item); + items = items.slice(0, limit); + + storage.setItem(items); + }, + remove(item) { + items = items.filter((x) => x.objectID !== item.objectID); + + storage.setItem(items); + }, + getAll() { + return items; + }, + }; +} diff --git a/packages/autocomplete-plugin-recent-searches/src/index.ts b/packages/autocomplete-plugin-recent-searches/src/index.ts new file mode 100644 index 000000000..072150b08 --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/src/index.ts @@ -0,0 +1 @@ +export { createRecentSearchesPlugin } from './createRecentSearchesPlugin'; diff --git a/packages/autocomplete-plugin-recent-searches/tsconfig.declaration.json b/packages/autocomplete-plugin-recent-searches/tsconfig.declaration.json new file mode 100644 index 000000000..1e0c6449f --- /dev/null +++ b/packages/autocomplete-plugin-recent-searches/tsconfig.declaration.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.declaration" +}