react-dadata-suggestions-reactoutsideclick
Version:
Just another one react component for dadata suggestions
346 lines (297 loc) • 9.01 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './styles/styles.less';
import SuggestionsList from './components/SuggestionsList';
import QueryInput from './components/QueryInput';
import ClickOutSide from './components/ReactOutsideClick';
import { handleKeyPress } from './handlers';
import Api from './api/FetchApi';
import { buildRequestBody } from "./api/helpers";
import { SHORT_TYPES } from "./constants/index";
import { isFunction, isEqual } from "./helpers";
class DadataSuggestions extends Component {
static propTypes = {
token: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
deferRequestBy: PropTypes.number.isRequired,
hint: PropTypes.string.isRequired,
minChars: PropTypes.number.isRequired,
geolocation: PropTypes.bool.isRequired,
query: PropTypes.string.isRequired,
service: PropTypes.string.isRequired,
highlighting: PropTypes.bool.isRequired,
specialRequestOptions: PropTypes.object,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
name: PropTypes.string,
//handlers:
onSelect: PropTypes.func.isRequired,
onChange: PropTypes.func,
onError: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
suggestionsFormatter: PropTypes.func,
selectedSuggestionFormatter: PropTypes.func,
};
static defaultProps = {
name: null,
token: '',
count: 10,
deferRequestBy: 300,
minChars: 3,
geolocation: true,
hint: 'Выберите вариант ниже или продолжите ввод',
query: '',
service: 'address',
highlighting: true,
};
constructor(props) {
super(props);
const {token, service, geolocation} = props;
this.api = new Api(token, service, geolocation);
this.handleKeyPress = handleKeyPress.bind(this);
this.fetchTimeoutId = null;
this.selectEventFired = false;
}
state = {
query: '',
suggestions: [],
selected: -1,
loading: false,
success: false,
error: false,
showSuggestions: false,
};
UNSAFE_componentWillMount() {
const { query } = this.props;
this.setState({ query });
}
componentDidMount() {
this._isMounted = true;
}
UNSAFE_componentWillReceiveProps(nextProps) {
// behaves like onChange behaves
const { query: newQuery, value: newValue } = nextProps;
const { value } = this.props;
const { query } = this.state;
// set external suggestion, passed through props
if (!!newValue && !isEqual(newValue, value)) {
this.setState({ suggestions: [newValue]});
this.selectSuggestion(0);
return
}
// this if block prevents state update
// on props changes caused by select event
if (this.selectEventFired) {
this.selectEventFired = false;
return;
}
if (newQuery !== query) {
this.handleChange(newQuery);
}
}
componentWillUnmount() {
this.clearFetchTimeout();
this._isMounted = false;
}
clearFetchTimeout = () => {
if (this.fetchTimeoutId) clearTimeout(this.fetchTimeoutId);
};
fetchData = (query) => {
if (this._isMounted) {
this.setState({
loading: true,
success: false,
});
}
const requestBody = buildRequestBody(query, this.props);
this.api.suggestions(requestBody)
.then(suggestions => {
if (this._isMounted) {
this.setState({
suggestions,
loading: false,
error: false,
success: true,
showSuggestions: true,
});
}
})
.catch(e => this.handleError(e));
};
searchWords = () => {
const { query } = this.state;
const searchWords = query.split(/[^-А-Яа-яЁё\d\w]+/);
const { service } = this.props;
if (service === Api.ADDRESS) {
return searchWords.filter(word => !SHORT_TYPES.includes(word));
}
return searchWords;
};
handleChange = (query) => {
const { deferRequestBy } = this.props;
this.clearFetchTimeout();
if (this._isMounted) {
this.setState({
query,
selected: -1
});
}
const { minChars } = this.props;
if (query.length >= minChars) {
this.fetchTimeoutId = setTimeout(() => {
this.fetchData(query);
}, deferRequestBy);
} else {
if (this._isMounted) {
this.setState({
suggestions: [],
showSuggestions: false,
success: false,
});
}
}
const { onChange } = this.props;
if (onChange) {
onChange(query);
}
};
handleBlur = (event) => {
this.makeListInvisible();
const { onBlur } = this.props;
if (isFunction(onBlur)) {
onBlur(event);
}
};
handleError = (e) => {
if (this._isMounted) {
this.setState({
error: true,
loading: false,
success: false,
});
}
const { onError } = this.props;
if (onError) {
onError(e);
}
};
selectSuggestion = (index) => {
this.setState(({suggestions}) => {
const selectedSuggestion = suggestions[index];
const query = this.selectedSuggestionFormatter(selectedSuggestion);
return {
selected: index,
query
}
});
this.selectEventFired = true;
};
handleSelect = (index) => () => {
const { selected } = this.state;
if (index !== selected) {
this.selectSuggestion(index);
}
this.makeListInvisible();
const selectedSuggestion = this.state.suggestions[index];
const { onSelect } = this.props;
onSelect(selectedSuggestion);
};
formatter = (suggestion, name) => {
const { [name]: customFormatter } = this.props;
if (customFormatter) {
return customFormatter(suggestion);
}
return suggestion.value;
};
suggestionsFormatter = (suggestion) => {
return this.formatter(suggestion, 'suggestionsFormatter')
};
selectedSuggestionFormatter = (suggestion) => {
return this.formatter(suggestion, 'selectedSuggestionFormatter')
};
subtextFormatter = (suggestion) => {
const { service } = this.props;
if (service === 'party') {
return `ИНН ${suggestion.data.inn}`;
}
return null;
};
makeListVisible = () => {
const { readOnly } = this.props;
if (readOnly) {
return;
}
const { showSuggestions } = this.state;
if (showSuggestions) {
return
}
this.setState({ showSuggestions: true });
};
handleFocus = (event) => {
const { readOnly } = this.props;
if (readOnly) {
return;
}
const { query, success, suggestions, selected, error } = this.state;
const { minChars } = this.props;
if (!!suggestions.length && selected === -1) {
this.makeListVisible();
} else if (query.length >= minChars && !success && !error) {
this.fetchData(query);
}
const { onFocus } = this.props;
if (isFunction(onFocus)) {
onFocus(event);
}
};
makeListInvisible = () => {
const { showSuggestions } = this.state;
if (!showSuggestions) {
return
}
this.setState({showSuggestions: false});
};
render() {
const {loading, query, showSuggestions, suggestions, selected} = this.state;
const {
name, placeholder, disabled, readOnly, // QuieryInput props
hint, highlighting, // SuggestionsList props
} = this.props;
return (
<ClickOutSide className="suggestions-container" onOutsideClick={this.handleBlur}>
<QueryInput
// props
name={ name }
placeholder={ placeholder }
disabled={ disabled }
readOnly={ readOnly }
// state
loading={ loading }
query={ query }
// handlers
onMouseDown={ this.makeListVisible }
onKeyPress={ this.handleKeyPress }
onFocus={ this.handleFocus }
onChange={ e => this.handleChange(e.target.value) }
/>
<SuggestionsList
// props
hint={ hint }
highlighting = { highlighting }
// state
suggestions={ suggestions }
selected={ selected }
visible={ showSuggestions }
// class attributes
onSelect={ this.handleSelect }
suggestionsFormatter={ this.suggestionsFormatter }
searchWords={ this.searchWords }
subtextFormatter = { this.subtextFormatter }
/>
</ClickOutSide>
);
}
}
export default DadataSuggestions;