nexxtway/react-rainbow

View on GitHub
src/components/GoogleAddressLookup/component.js

Summary

Maintainability
B
5 hrs
Test Coverage
/* eslint-disable react/sort-comp */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import withReduxForm from '../../libs/hocs/withReduxForm';
import Lookup from '../Lookup';
import LocationIcon from './icons/locationIcon';
import { uniqueId } from '../../libs/utils';
import getSuggestions from './helpers/getSuggestions';
import getSearchForOption from './helpers/getSearchForOption';
import getFormattedValue from './helpers/getFormattedValue';
import SelectedLocationIcon from './icons/selectedLocationIcon';
import RenderIf from '../RenderIf';
import getSearchParams from './helpers/getSearchParams';
import * as CustomPropTypes from './proptypes';
import StyledPoweredByGoogleContainer from './styled/poweredByGoogleContainer';
import StyledPoweredByGoogleLogo from './styled/poweredByGoogleLogo';

class PlacesLookupComponent extends Component {
    constructor(props) {
        super(props);
        this.lookupId = uniqueId('gaddrlookup-input');
        this.initialized = false;
        this.handleChange = this.handleChange.bind(this);
        this.handleSearch = this.handleSearch.bind(this);
        this.placesServiceRef = React.createRef();

        this.state = {
            isSearching: false,
            // TODO: remove
            places: [],
            suggestions: [],
        };
    }

    componentDidUpdate({ isScriptLoaded: prevIsScriptLoaded }) {
        // TODO: change this please, keep isInitialized as an state and don't
        // render anything until the
        // component is initialized
        const { isScriptLoaded, isScriptLoadSucceed } = this.props;
        if (!prevIsScriptLoaded && isScriptLoaded && isScriptLoadSucceed) {
            this.initComponent();
        }
    }

    getPlaceInfo(placeId) {
        this.setState({ isSearching: true });

        return new Promise((resolve, reject) => {
            this.placesService.getDetails(
                {
                    placeId,
                },
                (details, status) => {
                    this.setState({ isSearching: false });
                    if (status === window.google.maps.places.PlacesServiceStatus.OK) {
                        return resolve(details);
                    }
                    return reject(status);
                },
            );
        });
    }

    initComponent() {
        this.autocompleteService = new window.google.maps.places.AutocompleteService();
        this.placesService = new window.google.maps.places.PlacesService(
            this.placesServiceRef.current,
        );
        this.initialized = true;
    }

    handleChange(option) {
        const { onChange } = this.props;

        if (!option) {
            return onChange(null);
        }

        if (!option.id) {
            return onChange(option.data);
        }

        const { places } = this.state;
        const placePrediction = places.find(place => place.place_id === option.id);

        return this.getPlaceInfo(option.id)
            .then(result => {
                const resultValue = Object.assign(result, {
                    predictionInfo: placePrediction,
                });
                return onChange(resultValue);
            })
            .catch(() => {
                const resultValue = placePrediction;
                return onChange(resultValue);
            });
    }

    handleSearch(value) {
        if (!this.initialized) return;

        if (value) {
            this.searchGooglePlaces(value)
                .then(results => this.processSearchResults(results, value))
                .catch(() => this.handleError(value));
        } else if (!value) {
            this.clear();
        }
    }

    searchGooglePlaces(needle) {
        const { searchOptions } = this.props;
        const searchParams = getSearchParams(searchOptions);

        this.setState({ isSearching: true });

        return new Promise((resolve, reject) => {
            this.autocompleteService.getPlacePredictions(
                {
                    ...searchParams,
                    input: needle,
                },
                (predictions, status) => {
                    this.setState({ isSearching: false });
                    if (status !== window.google.maps.places.PlacesServiceStatus.OK) {
                        return reject(status);
                    }
                    return resolve(predictions);
                },
            );
        });
    }

    processSearchResults(results, searchValue) {
        this.setState({
            places: results,
            suggestions: getSuggestions(results, searchValue),
        });
    }

    handleError(searchValue) {
        this.setState({
            places: [],
            suggestions: [getSearchForOption(searchValue)],
        });
    }

    clear() {
        this.setState({
            places: [],
            suggestions: [],
        });
    }

    render() {
        const {
            style,
            className,
            label,
            error,
            readOnly,
            placeholder,
            disabled,
            tabIndex,
            required,
            id,
            name,
            value,
            labelAlignment,
            hideLabel,
            onClick,
            // onFocus,
            onBlur,
            variant,
            borderRadius,
        } = this.props;
        const { isSearching, suggestions } = this.state;
        const options = suggestions.length > 0 ? suggestions : null;
        return (
            <div id={id} style={style} className={className}>
                <Lookup
                    id={this.lookupId}
                    name={name}
                    label={label}
                    debounce
                    isLoading={isSearching}
                    placeholder={placeholder}
                    options={options}
                    labelAlignment={labelAlignment}
                    hideLabel={hideLabel}
                    readOnly={readOnly}
                    required={required}
                    tabIndex={tabIndex}
                    disabled={disabled}
                    value={getFormattedValue(value, false, <SelectedLocationIcon />)}
                    onChange={this.handleChange}
                    onSearch={this.handleSearch}
                    onClick={onClick}
                    // TODO: fix focus problem when used with redux form
                    // onFocus={onFocus}
                    onBlur={onBlur}
                    error={error}
                    icon={<LocationIcon />}
                    preferredSelectedOption={1}
                    variant={variant}
                    borderRadius={borderRadius}
                />
                <RenderIf isTrue={!error}>
                    <StyledPoweredByGoogleContainer>
                        <StyledPoweredByGoogleLogo />
                    </StyledPoweredByGoogleContainer>
                </RenderIf>
                <div ref={this.placesServiceRef} />
            </div>
        );
    }
}

PlacesLookupComponent.propTypes = {
    isScriptLoaded: PropTypes.bool.isRequired,
    isScriptLoadSucceed: PropTypes.bool.isRequired,
    searchOptions: CustomPropTypes.searchOptionsShape,
    label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
    labelAlignment: PropTypes.oneOf(['left', 'center', 'right']),
    hideLabel: PropTypes.bool,
    readOnly: PropTypes.bool,
    value: PropTypes.oneOfType([
        CustomPropTypes.predictionShape,
        CustomPropTypes.placeDetailsShape,
        PropTypes.string,
    ]),
    name: PropTypes.string,
    placeholder: PropTypes.string,
    required: PropTypes.bool,
    error: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
    disabled: PropTypes.bool,
    tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    id: PropTypes.string,
    className: PropTypes.string,
    style: PropTypes.object,
    onChange: PropTypes.func,
    onClick: PropTypes.func,
    // onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    variant: PropTypes.oneOf(['default', 'shaded', 'bare']),
    borderRadius: PropTypes.oneOf(['square', 'semi-square', 'semi-rounded', 'rounded']),
};

PlacesLookupComponent.defaultProps = {
    searchOptions: undefined,
    value: undefined,
    name: undefined,
    placeholder: null,
    required: false,
    readOnly: false,
    error: null,
    disabled: false,
    onChange: () => {},
    onClick: () => {},
    // onFocus: () => {},
    onBlur: () => {},
    tabIndex: undefined,
    label: undefined,
    className: undefined,
    style: undefined,
    id: undefined,
    labelAlignment: 'center',
    hideLabel: false,
    variant: 'default',
    borderRadius: 'rounded',
};

export default withReduxForm(PlacesLookupComponent);

export { PlacesLookupComponent as Component };