UNPKG

5.41 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/objectWithoutPropertiesLoose";
3import debounce from 'lodash.debounce';
4import PropTypes from 'prop-types';
5import React, { forwardRef, useCallback, useEffect, useRef } from 'react';
6import useForceUpdate from '@restart/hooks/useForceUpdate';
7import usePrevious from '@restart/hooks/usePrevious';
8import Typeahead from '../core/Typeahead';
9import { optionType } from '../propTypes';
10import { getDisplayName, isFunction, warn } from '../utils';
11var propTypes = {
12 /**
13 * Delay, in milliseconds, before performing search.
14 */
15 delay: PropTypes.number,
16
17 /**
18 * Whether or not a request is currently pending. Necessary for the
19 * container to know when new results are available.
20 */
21 isLoading: PropTypes.bool.isRequired,
22
23 /**
24 * Number of input characters that must be entered before showing results.
25 */
26 minLength: PropTypes.number,
27
28 /**
29 * Callback to perform when the search is executed.
30 */
31 onSearch: PropTypes.func.isRequired,
32
33 /**
34 * Options to be passed to the typeahead. Will typically be the query
35 * results, but can also be initial default options.
36 */
37 options: PropTypes.arrayOf(optionType),
38
39 /**
40 * Message displayed in the menu when there is no user input.
41 */
42 promptText: PropTypes.node,
43
44 /**
45 * Message displayed in the menu while the request is pending.
46 */
47 searchText: PropTypes.node,
48
49 /**
50 * Whether or not the component should cache query results.
51 */
52 useCache: PropTypes.bool
53};
54var defaultProps = {
55 delay: 200,
56 minLength: 2,
57 options: [],
58 promptText: 'Type to search...',
59 searchText: 'Searching...',
60 useCache: true
61};
62
63/**
64 * Logic that encapsulates common behavior and functionality around
65 * asynchronous searches, including:
66 *
67 * - Debouncing user input
68 * - Optional query caching
69 * - Search prompt and empty results behaviors
70 */
71export function useAsync(props) {
72 var allowNew = props.allowNew,
73 delay = props.delay,
74 emptyLabel = props.emptyLabel,
75 isLoading = props.isLoading,
76 minLength = props.minLength,
77 onInputChange = props.onInputChange,
78 onSearch = props.onSearch,
79 options = props.options,
80 promptText = props.promptText,
81 searchText = props.searchText,
82 useCache = props.useCache,
83 otherProps = _objectWithoutPropertiesLoose(props, ["allowNew", "delay", "emptyLabel", "isLoading", "minLength", "onInputChange", "onSearch", "options", "promptText", "searchText", "useCache"]);
84
85 var cacheRef = useRef({});
86 var handleSearchDebouncedRef = useRef();
87 var queryRef = useRef(props.defaultInputValue || '');
88 var forceUpdate = useForceUpdate();
89 var prevProps = usePrevious(props);
90 var handleSearch = useCallback(function (query) {
91 queryRef.current = query;
92
93 if (!query || minLength && query.length < minLength) {
94 return;
95 } // Use cached results, if applicable.
96
97
98 if (useCache && cacheRef.current[query]) {
99 // Re-render the component with the cached results.
100 forceUpdate();
101 return;
102 } // Perform the search.
103
104
105 onSearch(query);
106 }, [forceUpdate, minLength, onSearch, useCache]); // Set the debounced search function.
107
108 useEffect(function () {
109 handleSearchDebouncedRef.current = debounce(handleSearch, delay);
110 return function () {
111 handleSearchDebouncedRef.current && handleSearchDebouncedRef.current.cancel();
112 };
113 }, [delay, handleSearch]);
114 useEffect(function () {
115 // Ensure that we've gone from a loading to a completed state. Otherwise
116 // an empty response could get cached if the component updates during the
117 // request (eg: if the parent re-renders for some reason).
118 if (!isLoading && prevProps && prevProps.isLoading && useCache) {
119 cacheRef.current[queryRef.current] = options;
120 }
121 });
122
123 var getEmptyLabel = function getEmptyLabel() {
124 if (!queryRef.current.length) {
125 return promptText;
126 }
127
128 if (isLoading) {
129 return searchText;
130 }
131
132 return emptyLabel;
133 };
134
135 var handleInputChange = useCallback(function (query, e) {
136 onInputChange && onInputChange(query, e);
137 handleSearchDebouncedRef.current && handleSearchDebouncedRef.current(query);
138 }, [onInputChange]);
139 var cachedQuery = cacheRef.current[queryRef.current];
140 return _extends({}, otherProps, {
141 // Disable custom selections during a search if `allowNew` isn't a function.
142 allowNew: isFunction(allowNew) ? allowNew : allowNew && !isLoading,
143 emptyLabel: getEmptyLabel(),
144 isLoading: isLoading,
145 minLength: minLength,
146 onInputChange: handleInputChange,
147 options: useCache && cachedQuery ? cachedQuery : options
148 });
149}
150export function withAsync(Component) {
151 var AsyncTypeahead = /*#__PURE__*/forwardRef(function (props, ref) {
152 return /*#__PURE__*/React.createElement(Component, _extends({}, useAsync(props), {
153 ref: ref
154 }));
155 });
156 AsyncTypeahead.displayName = "withAsync(" + getDisplayName(Component) + ")"; // $FlowFixMe
157
158 AsyncTypeahead.propTypes = propTypes; // $FlowFixMe
159
160 AsyncTypeahead.defaultProps = defaultProps;
161 return AsyncTypeahead;
162}
163export default function asyncContainer(Component) {
164 /* istanbul ignore next */
165 warn(false, 'The `asyncContainer` export is deprecated; use `withAsync` instead.');
166 /* istanbul ignore next */
167
168 return withAsync(Component);
169}
\No newline at end of file