1 | import React from 'react';
|
---|
2 | import PropTypes from 'prop-types';
|
---|
3 | import debounce from 'lodash.debounce';
|
---|
4 |
|
---|
5 |
|
---|
6 | export class DebounceInput extends React.PureComponent {
|
---|
7 | static propTypes = {
|
---|
8 | element: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
---|
9 | type: PropTypes.string,
|
---|
10 | onChange: PropTypes.func.isRequired,
|
---|
11 | onKeyDown: PropTypes.func,
|
---|
12 | onBlur: PropTypes.func,
|
---|
13 | value: PropTypes.oneOfType([
|
---|
14 | PropTypes.string,
|
---|
15 | PropTypes.number
|
---|
16 | ]),
|
---|
17 | minLength: PropTypes.number,
|
---|
18 | debounceTimeout: PropTypes.number,
|
---|
19 | forceNotifyByEnter: PropTypes.bool,
|
---|
20 | forceNotifyOnBlur: PropTypes.bool,
|
---|
21 | inputRef: PropTypes.func
|
---|
22 | };
|
---|
23 |
|
---|
24 |
|
---|
25 | static defaultProps = {
|
---|
26 | element: 'input',
|
---|
27 | type: 'text',
|
---|
28 | onKeyDown: undefined,
|
---|
29 | onBlur: undefined,
|
---|
30 | value: undefined,
|
---|
31 | minLength: 0,
|
---|
32 | debounceTimeout: 100,
|
---|
33 | forceNotifyByEnter: true,
|
---|
34 | forceNotifyOnBlur: true,
|
---|
35 | inputRef: undefined
|
---|
36 | };
|
---|
37 |
|
---|
38 |
|
---|
39 | constructor(props) {
|
---|
40 | super(props);
|
---|
41 |
|
---|
42 | this.isDebouncing = false;
|
---|
43 | this.state = {value: typeof props.value === 'undefined' || props.value === null ? '' : props.value};
|
---|
44 |
|
---|
45 | const {debounceTimeout} = this.props;
|
---|
46 | this.createNotifier(debounceTimeout);
|
---|
47 | }
|
---|
48 |
|
---|
49 |
|
---|
50 | componentDidUpdate(prevProps) {
|
---|
51 | if (this.isDebouncing) {
|
---|
52 | return;
|
---|
53 | }
|
---|
54 | const {value, debounceTimeout} = this.props;
|
---|
55 |
|
---|
56 | const {debounceTimeout: oldTimeout, value: oldValue} = prevProps;
|
---|
57 | const {value: stateValue} = this.state;
|
---|
58 |
|
---|
59 | if (typeof value !== 'undefined' && oldValue !== value && stateValue !== value) {
|
---|
60 | // Update state.value if new value passed via props, yep re-render should happen
|
---|
61 | // eslint-disable-next-line react/no-did-update-set-state
|
---|
62 | this.setState({value});
|
---|
63 | }
|
---|
64 | if (debounceTimeout !== oldTimeout) {
|
---|
65 | this.createNotifier(debounceTimeout);
|
---|
66 | }
|
---|
67 | }
|
---|
68 |
|
---|
69 |
|
---|
70 | componentWillUnmount() {
|
---|
71 | if (this.flush) {
|
---|
72 | this.flush();
|
---|
73 | }
|
---|
74 | }
|
---|
75 |
|
---|
76 |
|
---|
77 | onChange = event => {
|
---|
78 | event.persist();
|
---|
79 |
|
---|
80 | const {value: oldValue} = this.state;
|
---|
81 | const {minLength} = this.props;
|
---|
82 |
|
---|
83 | this.setState({value: event.target.value}, () => {
|
---|
84 | const {value} = this.state;
|
---|
85 |
|
---|
86 | if (value.length >= minLength) {
|
---|
87 | this.notify(event);
|
---|
88 | return;
|
---|
89 | }
|
---|
90 |
|
---|
91 | // If user hits backspace and goes below minLength consider it cleaning the value
|
---|
92 | if (oldValue.length > value.length) {
|
---|
93 | this.notify({...event, target: {...event.target, value: ''}});
|
---|
94 | }
|
---|
95 | });
|
---|
96 | };
|
---|
97 |
|
---|
98 |
|
---|
99 | onKeyDown = event => {
|
---|
100 | if (event.key === 'Enter') {
|
---|
101 | this.forceNotify(event);
|
---|
102 | }
|
---|
103 | // Invoke original onKeyDown if present
|
---|
104 | const {onKeyDown} = this.props;
|
---|
105 | if (onKeyDown) {
|
---|
106 | event.persist();
|
---|
107 | onKeyDown(event);
|
---|
108 | }
|
---|
109 | };
|
---|
110 |
|
---|
111 |
|
---|
112 | onBlur = event => {
|
---|
113 | this.forceNotify(event);
|
---|
114 | // Invoke original onBlur if present
|
---|
115 | const {onBlur} = this.props;
|
---|
116 | if (onBlur) {
|
---|
117 | event.persist();
|
---|
118 | onBlur(event);
|
---|
119 | }
|
---|
120 | };
|
---|
121 |
|
---|
122 |
|
---|
123 | createNotifier = debounceTimeout => {
|
---|
124 | if (debounceTimeout < 0) {
|
---|
125 | this.notify = () => null;
|
---|
126 | } else if (debounceTimeout === 0) {
|
---|
127 | this.notify = this.doNotify;
|
---|
128 | } else {
|
---|
129 | const debouncedChangeFunc = debounce(event => {
|
---|
130 | this.isDebouncing = false;
|
---|
131 | this.doNotify(event);
|
---|
132 | }, debounceTimeout);
|
---|
133 |
|
---|
134 | this.notify = event => {
|
---|
135 | this.isDebouncing = true;
|
---|
136 | debouncedChangeFunc(event);
|
---|
137 | };
|
---|
138 |
|
---|
139 | this.flush = () => debouncedChangeFunc.flush();
|
---|
140 |
|
---|
141 | this.cancel = () => {
|
---|
142 | this.isDebouncing = false;
|
---|
143 | debouncedChangeFunc.cancel();
|
---|
144 | };
|
---|
145 | }
|
---|
146 | };
|
---|
147 |
|
---|
148 |
|
---|
149 | doNotify = (...args) => {
|
---|
150 | const {onChange} = this.props;
|
---|
151 |
|
---|
152 | onChange(...args);
|
---|
153 | };
|
---|
154 |
|
---|
155 |
|
---|
156 | forceNotify = event => {
|
---|
157 | const {debounceTimeout} = this.props;
|
---|
158 | if (!this.isDebouncing && debounceTimeout > 0) {
|
---|
159 | return;
|
---|
160 | }
|
---|
161 |
|
---|
162 | if (this.cancel) {
|
---|
163 | this.cancel();
|
---|
164 | }
|
---|
165 |
|
---|
166 | const {value} = this.state;
|
---|
167 | const {minLength} = this.props;
|
---|
168 |
|
---|
169 | if (value.length >= minLength) {
|
---|
170 | this.doNotify(event);
|
---|
171 | } else {
|
---|
172 | this.doNotify({...event, target: {...event.target, value}});
|
---|
173 | }
|
---|
174 | };
|
---|
175 |
|
---|
176 |
|
---|
177 | render() {
|
---|
178 | const {
|
---|
179 | element,
|
---|
180 | onChange: _onChange,
|
---|
181 | value: _value,
|
---|
182 | minLength: _minLength,
|
---|
183 | debounceTimeout: _debounceTimeout,
|
---|
184 | forceNotifyByEnter,
|
---|
185 | forceNotifyOnBlur,
|
---|
186 | onKeyDown,
|
---|
187 | onBlur,
|
---|
188 | inputRef,
|
---|
189 | ...props
|
---|
190 | } = this.props;
|
---|
191 | const {value} = this.state;
|
---|
192 |
|
---|
193 | let maybeOnKeyDown;
|
---|
194 | if (forceNotifyByEnter) {
|
---|
195 | maybeOnKeyDown = {onKeyDown: this.onKeyDown};
|
---|
196 | } else if (onKeyDown) {
|
---|
197 | maybeOnKeyDown = {onKeyDown};
|
---|
198 | } else {
|
---|
199 | maybeOnKeyDown = {};
|
---|
200 | }
|
---|
201 |
|
---|
202 | let maybeOnBlur;
|
---|
203 | if (forceNotifyOnBlur) {
|
---|
204 | maybeOnBlur = {onBlur: this.onBlur};
|
---|
205 | } else if (onBlur) {
|
---|
206 | maybeOnBlur = {onBlur};
|
---|
207 | } else {
|
---|
208 | maybeOnBlur = {};
|
---|
209 | }
|
---|
210 |
|
---|
211 | const maybeRef = inputRef ? {ref: inputRef} : {};
|
---|
212 |
|
---|
213 | return React.createElement(element, {
|
---|
214 | ...props,
|
---|
215 | onChange: this.onChange,
|
---|
216 | value,
|
---|
217 | ...maybeOnKeyDown,
|
---|
218 | ...maybeOnBlur,
|
---|
219 | ...maybeRef
|
---|
220 | });
|
---|
221 | }
|
---|
222 | }
|
---|