import React, { KeyboardEvent, Component, ReactElement } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { css } from 'emotion'; import { default as IconSearch } from '@material-ui/icons/Search'; import InputAdornment from '@material-ui/core/InputAdornment'; import debounce from 'lodash/debounce'; import API from '../../utils/api'; import AutoComplete from '../AutoComplete'; import colors from '../../utils/styles/colors'; export interface State { search: string; suggestions: unknown[]; loading: boolean; loaded: boolean; error: boolean; } interface AbortControllerInterface { signal: () => void; abort: () => void; } export type cancelAllSearchRequests = () => void; export type handlePackagesClearRequested = () => void; export type handleSearch = (event: KeyboardEvent, { newValue, method }: { newValue: string; method: string }) => void; export type handleClickSearch = (event: KeyboardEvent, { suggestionValue, method }: { suggestionValue: object[]; method: string }) => void; export type handleFetchPackages = ({ value: string }) => Promise; export type onBlur = (event: KeyboardEvent) => void; const CONSTANTS = { API_DELAY: 300, PLACEHOLDER_TEXT: 'Search Packages', ABORT_ERROR: 'AbortError', }; export class Search extends Component, State> { constructor(props: RouteComponentProps<{}>) { super(props); this.state = { search: '', suggestions: [], // loading: A boolean value to indicate that request is in pending state. loading: false, // loaded: A boolean value to indicate that result has been loaded. loaded: false, // error: A boolean value to indicate API error. error: false, }; this.requestList = []; } public render(): ReactElement { const { suggestions, search, loaded, loading, error } = this.state; return ( ); } /** * Cancel all the requests which are in pending state. */ private cancelAllSearchRequests: cancelAllSearchRequests = () => { this.requestList.forEach(request => request.abort()); this.requestList = []; }; /** * Cancel all the request from list and make request list empty. */ private handlePackagesClearRequested: handlePackagesClearRequested = () => { this.setState({ suggestions: [], }); }; /** * onChange method for the input element. */ private handleSearch: handleSearch = (event, { newValue, method }) => { // stops event bubbling event.stopPropagation(); if (method === 'type') { const value = newValue.trim(); this.setState( { search: value, loading: true, loaded: false, error: false, }, () => { /** * A use case where User keeps adding and removing value in input field, * so we cancel all the existing requests when input is empty. */ if (value.length === 0) { this.cancelAllSearchRequests(); } } ); } }; /** * When an user select any package by clicking or pressing return key. */ private handleClickSearch = ( event: React.KeyboardEvent, { suggestionValue, method }: { suggestionValue: string[]; method: string } ): void | undefined => { const { history } = this.props; // stops event bubbling event.stopPropagation(); switch (method) { case 'click': case 'enter': this.setState({ search: '' }); history.push(`/-/web/detail/${suggestionValue}`); break; } }; /** * Fetch packages from API. * For AbortController see: https://developer.mozilla.org/en-US/docs/Web/API/AbortController */ private handleFetchPackages: handleFetchPackages = async ({ value }) => { try { // @ts-ignore const controller = new window.AbortController(); const signal = controller.signal; // Keep track of search requests. this.requestList.push(controller); const suggestions = await API.request(`search/${encodeURIComponent(value)}`, 'GET', { signal }); // @ts-ignore this.setState({ suggestions, loaded: true, }); } catch (error) { /** * AbortError is not the API error. * It means browser has cancelled the API request. */ if (error.name === CONSTANTS.ABORT_ERROR) { this.setState({ error: false, loaded: false }); } else { this.setState({ error: true, loaded: false }); } } finally { this.setState({ loading: false }); } }; private requestList: AbortControllerInterface[]; public getAdorment(): JSX.Element { return ( ); } /** * As user focuses out from input, we cancel all the request from requestList * and set the API state parameters to default boolean values. */ private handleOnBlur: onBlur = event => { // stops event bubbling event.stopPropagation(); this.setState( { loaded: false, loading: false, error: false, }, () => this.cancelAllSearchRequests() ); }; } export default withRouter(Search);