Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: dcc.Input accepts a number for its debounce argument #2593

Merged
merged 7 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions components/dash-core-components/src/components/Input.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ export default class Input extends PureComponent {
this.onChange = this.onChange.bind(this);
this.onEvent = this.onEvent.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.debounceEvent = this.debounceEvent.bind(this);
this.setInputValue = this.setInputValue.bind(this);
this.setPropValue = this.setPropValue.bind(this);
}

UNSAFE_componentWillReceiveProps(nextProps) {
const {value} = this.input.current;
if (this.state?.pendingEvent) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State should be defined in constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Changed in 0198e20

// avoid updating the input while awaiting a debounced event
return;
}
const valueAsNumber = convert(value);
this.setInputValue(
isNil(valueAsNumber) ? value : valueAsNumber,
Expand Down Expand Up @@ -121,6 +126,23 @@ export default class Input extends PureComponent {
} else {
this.props.setProps({value});
}
this.setState({pendingEvent: undefined});
}

debounceEvent(time = 0.5) {
const {value} = this.input.current;
const MILLISECONDS = 1000;
T4rk1n marked this conversation as resolved.
Show resolved Hide resolved
time = time * MILLISECONDS;
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved

window.clearTimeout(this.state?.pendingEvent);
const pendingEvent = window.setTimeout(() => {
this.onEvent();
}, time);

this.setState({
value,
pendingEvent,
});
}

onBlur() {
Expand All @@ -129,7 +151,7 @@ export default class Input extends PureComponent {
n_blur_timestamp: Date.now(),
});
this.input.current.checkValidity();
return this.props.debounce && this.onEvent();
return this.props.debounce === true && this.onEvent();
}

onKeyPress(e) {
Expand All @@ -140,14 +162,22 @@ export default class Input extends PureComponent {
});
this.input.current.checkValidity();
}
return this.props.debounce && e.key === 'Enter' && this.onEvent();
return (
this.props.debounce === true && e.key === 'Enter' && this.onEvent()
);
}

onChange() {
if (!this.props.debounce) {
const {debounce} = this.props;
if (debounce) {
if (Number.isFinite(debounce)) {
this.debounceEvent(debounce);
}
if (this.props.type !== 'number') {
this.setState({value: this.input.current.value});
}
} else {
this.onEvent();
} else if (this.props.type !== 'number') {
this.setState({value: this.input.current.value});
T4rk1n marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down Expand Up @@ -188,9 +218,11 @@ Input.propTypes = {

/**
* If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
* If it's false, it will sent the value back on every change.
* If it's false, it will send the value back on every change.
* If a number, it will not send anything back to the Dash server until the user has stopped
* typing for that number of seconds.
*/
debounce: PropTypes.bool,
debounce: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,68 @@ def range_out(val):
return val

yield app


@pytest.fixture(scope="module")
def debounce_text_app():
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(
id="input-slow",
debounce=3,
placeholder="long wait",
),
html.Div(id="div-slow"),
dcc.Input(
id="input-fast",
debounce=0.25,
placeholder="short wait",
),
html.Div(id="div-fast"),
]
)

@app.callback(
[Output("div-slow", "children"), Output("div-fast", "children")],
[Input("input-slow", "value"), Input("input-fast", "value")],
)
def render(slow_val, fast_val):
return [slow_val, fast_val]

yield app


@pytest.fixture(scope="module")
def debounce_number_app():
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(
id="input-slow",
debounce=3,
type="number",
placeholder="long wait",
),
html.Div(id="div-slow"),
dcc.Input(
id="input-fast",
debounce=0.25,
type="number",
min=10,
max=10000,
step=3,
placeholder="short wait",
),
html.Div(id="div-fast"),
]
)

@app.callback(
[Output("div-slow", "children"), Output("div-fast", "children")],
[Input("input-slow", "value"), Input("input-fast", "value")],
)
def render(slow_val, fast_val):
return [slow_val, fast_val]

yield app
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from selenium.common import TimeoutException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
import pytest


def test_debounce_text_by_time(dash_dcc, debounce_text_app):
dash_dcc.start_server(debounce_text_app)

# expect that a long debounce does not call back in a short amount of time
elem = dash_dcc.find_element("#input-slow")
elem.send_keys("unit test slow")
with pytest.raises(TimeoutException):
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='unit test slow']")
)

# but do expect that it is eventually called
assert dash_dcc.wait_for_text_to_equal(
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved
"#div-slow", "unit test slow"
), "long debounce is eventually called back"

# expect that a short debounce calls back within a short amount of time
elem = dash_dcc.find_element("#input-fast")
elem.send_keys("unit test fast")
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='unit test fast']")
)

assert dash_dcc.get_logs() == []


def test_debounce_number_by_time(dash_dcc, debounce_number_app):
dash_dcc.start_server(debounce_number_app)

# expect that a long debounce does not call back in a short amount of time
elem = dash_dcc.find_element("#input-slow")
elem.send_keys("12345")
with pytest.raises(TimeoutException):
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='12345']")
)

# but do expect that it is eventually called
assert dash_dcc.wait_for_text_to_equal(
"#div-slow", "12345"
), "long debounce is eventually called back"

# expect that a short debounce calls back within a short amount of time
elem = dash_dcc.find_element("#input-fast")
elem.send_keys("67890")
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='67890']")
)

assert dash_dcc.get_logs() == []
2 changes: 1 addition & 1 deletion requires-all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
redis>=3.5.3
celery[redis]>=5.1.2
# Dependencies used by CI on github.com/plotly/dash
black==21.6b0
black==22.3.0
dash-flow-example==0.0.5
dash-dangerously-set-inner-html
flake8==3.9.2
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/devtools/test_props_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"fail": True,
"name": 'simple "not a boolean" check',
"component": dcc.Input,
"props": {"debounce": 0},
"props": {"multiple": 0},
},
"missing-required-nested-prop": {
"fail": True,
Expand Down