import React from 'react'; import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; export class DebounceInput extends React.PureComponent { static propTypes = { element: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), type: PropTypes.string, onChange: PropTypes.func.isRequired, onKeyDown: PropTypes.func, onBlur: PropTypes.func, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), minLength: PropTypes.number, debounceTimeout: PropTypes.number, forceNotifyByEnter: PropTypes.bool, forceNotifyOnBlur: PropTypes.bool, inputRef: PropTypes.func }; static defaultProps = { element: 'input', type: 'text', onKeyDown: undefined, onBlur: undefined, value: undefined, minLength: 0, debounceTimeout: 100, forceNotifyByEnter: true, forceNotifyOnBlur: true, inputRef: undefined }; constructor(props) { super(props); this.isDebouncing = false; this.state = {value: typeof props.value === 'undefined' || props.value === null ? '' : props.value}; const {debounceTimeout} = this.props; this.createNotifier(debounceTimeout); } componentDidUpdate(prevProps) { if (this.isDebouncing) { return; } const {value, debounceTimeout} = this.props; const {debounceTimeout: oldTimeout, value: oldValue} = prevProps; const {value: stateValue} = this.state; if (typeof value !== 'undefined' && oldValue !== value && stateValue !== value) { // Update state.value if new value passed via props, yep re-render should happen // eslint-disable-next-line react/no-did-update-set-state this.setState({value}); } if (debounceTimeout !== oldTimeout) { this.createNotifier(debounceTimeout); } } componentWillUnmount() { if (this.flush) { this.flush(); } } onChange = event => { event.persist(); const {value: oldValue} = this.state; const {minLength} = this.props; this.setState({value: event.target.value}, () => { const {value} = this.state; if (value.length >= minLength) { this.notify(event); return; } // If user hits backspace and goes below minLength consider it cleaning the value if (oldValue.length > value.length) { this.notify({...event, target: {...event.target, value: ''}}); } }); }; onKeyDown = event => { if (event.key === 'Enter') { this.forceNotify(event); } // Invoke original onKeyDown if present const {onKeyDown} = this.props; if (onKeyDown) { event.persist(); onKeyDown(event); } }; onBlur = event => { this.forceNotify(event); // Invoke original onBlur if present const {onBlur} = this.props; if (onBlur) { event.persist(); onBlur(event); } }; createNotifier = debounceTimeout => { if (debounceTimeout < 0) { this.notify = () => null; } else if (debounceTimeout === 0) { this.notify = this.doNotify; } else { const debouncedChangeFunc = debounce(event => { this.isDebouncing = false; this.doNotify(event); }, debounceTimeout); this.notify = event => { this.isDebouncing = true; debouncedChangeFunc(event); }; this.flush = () => debouncedChangeFunc.flush(); this.cancel = () => { this.isDebouncing = false; debouncedChangeFunc.cancel(); }; } }; doNotify = (...args) => { const {onChange} = this.props; onChange(...args); }; forceNotify = event => { const {debounceTimeout} = this.props; if (!this.isDebouncing && debounceTimeout > 0) { return; } if (this.cancel) { this.cancel(); } const {value} = this.state; const {minLength} = this.props; if (value.length >= minLength) { this.doNotify(event); } else { this.doNotify({...event, target: {...event.target, value}}); } }; render() { const { element, onChange: _onChange, value: _value, minLength: _minLength, debounceTimeout: _debounceTimeout, forceNotifyByEnter, forceNotifyOnBlur, onKeyDown, onBlur, inputRef, ...props } = this.props; const {value} = this.state; let maybeOnKeyDown; if (forceNotifyByEnter) { maybeOnKeyDown = {onKeyDown: this.onKeyDown}; } else if (onKeyDown) { maybeOnKeyDown = {onKeyDown}; } else { maybeOnKeyDown = {}; } let maybeOnBlur; if (forceNotifyOnBlur) { maybeOnBlur = {onBlur: this.onBlur}; } else if (onBlur) { maybeOnBlur = {onBlur}; } else { maybeOnBlur = {}; } const maybeRef = inputRef ? {ref: inputRef} : {}; return React.createElement(element, { ...props, onChange: this.onChange, value, ...maybeOnKeyDown, ...maybeOnBlur, ...maybeRef }); } }