Skip to content

Commit 342126a

Browse files
committed
Add ability to debounce an input field by amount of time
1 parent a7a12d1 commit 342126a

File tree

4 files changed

+164
-8
lines changed

4 files changed

+164
-8
lines changed

components/dash-core-components/src/components/Input.react.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class Input extends PureComponent {
2626
this.onChange = this.onChange.bind(this);
2727
this.onEvent = this.onEvent.bind(this);
2828
this.onKeyPress = this.onKeyPress.bind(this);
29+
this.debounceEvent = this.debounceEvent.bind(this);
2930
this.setInputValue = this.setInputValue.bind(this);
3031
this.setPropValue = this.setPropValue.bind(this);
3132
}
@@ -123,13 +124,30 @@ export default class Input extends PureComponent {
123124
}
124125
}
125126

127+
debounceEvent() {
128+
const {value} = this.input.current;
129+
let {debounce} = this.props;
130+
debounce = Number.isFinite(debounce) ? debounce * 1000 : 500;
131+
132+
window.clearTimeout(this.state?.pendingEvent);
133+
const pendingEvent = window.setTimeout(() => {
134+
this.onEvent();
135+
}, debounce);
136+
137+
this.setState({
138+
...this.state,
139+
value,
140+
pendingEvent,
141+
});
142+
}
143+
126144
onBlur() {
127145
this.props.setProps({
128146
n_blur: this.props.n_blur + 1,
129147
n_blur_timestamp: Date.now(),
130148
});
131149
this.input.current.checkValidity();
132-
return this.props.debounce && this.onEvent();
150+
return this.props.debounce === true && this.onEvent();
133151
}
134152

135153
onKeyPress(e) {
@@ -140,15 +158,29 @@ export default class Input extends PureComponent {
140158
});
141159
this.input.current.checkValidity();
142160
}
143-
return this.props.debounce && e.key === 'Enter' && this.onEvent();
161+
return this.props.debounce === true && e.key === 'Enter' && this.onEvent();
144162
}
145163

146164
onChange() {
147-
if (!this.props.debounce) {
165+
if (this.props.debounce) {
166+
if (this.props.type !== 'number') {
167+
// this.setState({value: this.input.current.value});
168+
}
169+
if (typeof this.props.debounce === 'number') {
170+
this.debounceEvent();
171+
}
172+
} else {
148173
this.onEvent();
149-
} else if (this.props.type !== 'number') {
150-
this.setState({value: this.input.current.value});
151174
}
175+
176+
177+
// if (!this.props.debounce) {
178+
// this.onEvent();
179+
// } else if (this.props.type !== 'number') {
180+
// this.setState({value: this.input.current.value});
181+
// } else if (typeof this.props.debounce === 'number') {
182+
// this.debounceEvent();
183+
// }
152184
}
153185
}
154186

@@ -188,9 +220,11 @@ Input.propTypes = {
188220

189221
/**
190222
* If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
191-
* If it's false, it will sent the value back on every change.
223+
* If it's false, it will send the value back on every change.
224+
* If a number, it will wait for the user to stop changing the input for that number of seconds before
225+
* sending the value back
192226
*/
193-
debounce: PropTypes.bool,
227+
debounce: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
194228

195229
/**
196230
* A hint to the user of what can be entered in the control . The placeholder text must not contain carriage returns or line-feeds. Note: Do not use the placeholder attribute instead of a <label> element, their purposes are different. The <label> attribute describes the role of the form element (i.e. it indicates what kind of information is expected), and the placeholder attribute is a hint about the format that the content should take. There are cases in which the placeholder attribute is never displayed to the user, so the form must be understandable without it.

components/dash-core-components/tests/integration/input/conftest.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,68 @@ def range_out(val):
5757
return val
5858

5959
yield app
60+
61+
@pytest.fixture(scope="module")
62+
def debounce_text_app():
63+
app = Dash(__name__)
64+
app.layout = html.Div(
65+
[
66+
dcc.Input(
67+
id="input-slow",
68+
debounce=3,
69+
placeholder="long wait",
70+
),
71+
html.Div(id="div-slow"),
72+
73+
dcc.Input(
74+
id="input-fast",
75+
debounce=0.25,
76+
placeholder="short wait",
77+
),
78+
html.Div(id="div-fast"),
79+
]
80+
)
81+
82+
@app.callback(
83+
[Output("div-slow", "children"), Output("div-fast", "children")],
84+
[Input("input-slow", "value"), Input("input-fast", "value")],
85+
)
86+
def render(slow_val, fast_val):
87+
return [slow_val, fast_val]
88+
89+
yield app
90+
91+
@pytest.fixture(scope="module")
92+
def debounce_number_app():
93+
app = Dash(__name__)
94+
app.layout = html.Div(
95+
[
96+
dcc.Input(
97+
id="input-slow",
98+
debounce=3,
99+
type="number",
100+
placeholder="long wait",
101+
),
102+
html.Div(id="div-slow"),
103+
104+
dcc.Input(
105+
id="input-fast",
106+
debounce=0.25,
107+
type="number",
108+
min=10,
109+
max=10000,
110+
step=3,
111+
placeholder="short wait",
112+
),
113+
html.Div(id="div-fast"),
114+
]
115+
)
116+
117+
@app.callback(
118+
[Output("div-slow", "children"), Output("div-fast", "children")],
119+
[Input("input-slow", "value"), Input("input-fast", "value")],
120+
)
121+
def render(slow_val, fast_val):
122+
return [slow_val, fast_val]
123+
124+
yield app
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from selenium.common import TimeoutException
2+
from selenium.webdriver.support.wait import WebDriverWait
3+
from selenium.webdriver.common.by import By
4+
import pytest
5+
6+
def test_debounce_text_by_time(dash_dcc, debounce_text_app):
7+
dash_dcc.start_server(debounce_text_app)
8+
9+
# expect that a long debounce does not call back in a short amount of time
10+
elem = dash_dcc.find_element("#input-slow")
11+
elem.send_keys('unit test slow')
12+
with pytest.raises(TimeoutException):
13+
WebDriverWait(dash_dcc.driver, timeout=1).until(
14+
lambda d: d.find_element(By.XPATH, "//*[text()='unit test slow']")
15+
)
16+
17+
# but do expect that it is eventually called
18+
assert dash_dcc.wait_for_text_to_equal(
19+
"#div-slow", "unit test slow"
20+
), "long debounce is eventually called back"
21+
22+
23+
# expect that a short debounce calls back within a short amount of time
24+
elem = dash_dcc.find_element("#input-fast")
25+
elem.send_keys('unit test fast')
26+
WebDriverWait(dash_dcc.driver, timeout=1).until(
27+
lambda d: d.find_element(By.XPATH, "//*[text()='unit test fast']")
28+
)
29+
30+
assert dash_dcc.get_logs() == []
31+
32+
33+
def test_debounce_number_by_time(dash_dcc, debounce_number_app):
34+
dash_dcc.start_server(debounce_number_app)
35+
36+
# expect that a long debounce does not call back in a short amount of time
37+
elem = dash_dcc.find_element("#input-slow")
38+
elem.send_keys('12345')
39+
with pytest.raises(TimeoutException):
40+
WebDriverWait(dash_dcc.driver, timeout=1).until(
41+
lambda d: d.find_element(By.XPATH, "//*[text()='12345']")
42+
)
43+
44+
# but do expect that it is eventually called
45+
assert dash_dcc.wait_for_text_to_equal(
46+
"#div-slow", "12345"
47+
), "long debounce is eventually called back"
48+
49+
50+
# expect that a short debounce calls back within a short amount of time
51+
elem = dash_dcc.find_element("#input-fast")
52+
elem.send_keys('67890')
53+
WebDriverWait(dash_dcc.driver, timeout=1).until(
54+
lambda d: d.find_element(By.XPATH, "//*[text()='67890']")
55+
)
56+
57+
assert dash_dcc.get_logs() == []

requires-all.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
redis>=3.5.3
33
celery[redis]>=5.1.2
44
# Dependencies used by CI on github.com/plotly/dash
5-
black==21.6b0
5+
black==22.3.0
66
dash-flow-example==0.0.5
77
dash-dangerously-set-inner-html
88
flake8==3.9.2

0 commit comments

Comments
 (0)