Skip to content

Commit 692298e

Browse files
authored
Merge pull request #3294 from BSd3v/feat/3067
adds `allow_optional` to State and Input to place no value as the placeholders
2 parents 4195561 + 900da07 commit 692298e

File tree

4 files changed

+149
-18
lines changed

4 files changed

+149
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
88
## Fixed
99
- [#3279](https://github.com/plotly/dash/pull/3279) Fix an issue where persisted values were incorrectly pruned when updated via callback. Now, callback returned values are correctly stored in the persistence storage. Fix [#2678](https://github.com/plotly/dash/issues/2678)
1010

11+
## Added
12+
- [#3294](https://github.com/plotly/dash/pull/3294) Added the ability to pass `allow_optional` to Input and State to allow callbacks to work even if these components are not in the dash layout.
13+
1114
## [3.0.4] - 2025-04-24
1215

1316
## Fixed

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,27 @@ function unwrapIfNotMulti(
122122

123123
if (idProps.length !== 1) {
124124
if (!idProps.length) {
125-
const isStr = typeof spec.id === 'string';
126-
msg =
127-
'A nonexistent object was used in an `' +
128-
depType +
129-
'` of a Dash callback. The id of this object is ' +
130-
(isStr
131-
? '`' + spec.id + '`'
132-
: JSON.stringify(spec.id) +
133-
(anyVals ? ' with MATCH values ' + anyVals : '')) +
134-
' and the property is `' +
135-
spec.property +
136-
(isStr
137-
? '`. The string ids in the current layout are: [' +
138-
keys(paths.strs).join(', ') +
139-
']'
140-
: '`. The wildcard ids currently available are logged above.');
125+
if (spec.allow_optional) {
126+
idProps = [{...spec, value: null}];
127+
msg = '';
128+
} else {
129+
const isStr = typeof spec.id === 'string';
130+
msg =
131+
'A nonexistent object was used in an `' +
132+
depType +
133+
'` of a Dash callback. The id of this object is ' +
134+
(isStr
135+
? '`' + spec.id + '`'
136+
: JSON.stringify(spec.id) +
137+
(anyVals ? ' with MATCH values ' + anyVals : '')) +
138+
' and the property is `' +
139+
spec.property +
140+
(isStr
141+
? '`. The string ids in the current layout are: [' +
142+
keys(paths.strs).join(', ') +
143+
']'
144+
: '`. The wildcard ids currently available are logged above.');
145+
}
141146
} else {
142147
msg =
143148
'Multiple objects were found for an `' +
@@ -203,7 +208,6 @@ function fillVals(
203208
// That's a real problem, so throw the first message as an error.
204209
refErr(errors, paths);
205210
}
206-
207211
return inputVals;
208212
}
209213

dash/dependencies.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class DashDependency: # pylint: disable=too-few-public-methods
3535
allow_duplicate: bool
3636
component_property: str
3737
allowed_wildcards: Sequence[_Wildcard]
38+
allow_optional: bool
3839

3940
def __init__(self, component_id: ComponentIdType, component_property: str):
4041

@@ -45,6 +46,7 @@ def __init__(self, component_id: ComponentIdType, component_property: str):
4546

4647
self.component_property = component_property
4748
self.allow_duplicate = False
49+
self.allow_optional = False
4850

4951
def __str__(self):
5052
return f"{self.component_id_str()}.{self.component_property}"
@@ -56,7 +58,10 @@ def component_id_str(self) -> str:
5658
return stringify_id(self.component_id)
5759

5860
def to_dict(self) -> dict:
59-
return {"id": self.component_id_str(), "property": self.component_property}
61+
specs = {"id": self.component_id_str(), "property": self.component_property}
62+
if self.allow_optional:
63+
specs["allow_optional"] = True
64+
return specs
6065

6166
def __eq__(self, other):
6267
"""
@@ -134,12 +139,30 @@ def __init__(
134139
class Input(DashDependency): # pylint: disable=too-few-public-methods
135140
"""Input of callback: trigger an update when it is updated."""
136141

142+
def __init__(
143+
self,
144+
component_id: ComponentIdType,
145+
component_property: str,
146+
allow_optional: bool = False,
147+
):
148+
super().__init__(component_id, component_property)
149+
self.allow_optional = allow_optional
150+
137151
allowed_wildcards = (MATCH, ALL, ALLSMALLER)
138152

139153

140154
class State(DashDependency): # pylint: disable=too-few-public-methods
141155
"""Use the value of a State in a callback but don't trigger updates."""
142156

157+
def __init__(
158+
self,
159+
component_id: ComponentIdType,
160+
component_property: str,
161+
allow_optional: bool = False,
162+
):
163+
super().__init__(component_id, component_property)
164+
self.allow_optional = allow_optional
165+
143166
allowed_wildcards = (MATCH, ALL, ALLSMALLER)
144167

145168

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from dash import Dash, html, Output, Input, State, no_update
2+
3+
4+
def test_cbop001_optional_input(dash_duo):
5+
app = Dash(suppress_callback_exceptions=True)
6+
7+
app.layout = html.Div(
8+
[
9+
html.Button(id="button1", children="Button 1"),
10+
html.Div(id="button-container"),
11+
html.Div(id="test-out"),
12+
html.Div(id="test-out2"),
13+
]
14+
)
15+
16+
@app.callback(
17+
Output("button-container", "children"),
18+
Input("button1", "n_clicks"),
19+
State("button-container", "children"),
20+
prevent_initial_call=True,
21+
)
22+
def _(_, c):
23+
if not c:
24+
return html.Button(id="button2", children="Button 2")
25+
return no_update
26+
27+
@app.callback(
28+
Output("test-out", "children"),
29+
Input("button1", "n_clicks"),
30+
Input("button2", "n_clicks", allow_optional=True),
31+
prevent_inital_call=True,
32+
)
33+
def display(n, n2):
34+
return f"{n} - {n2}"
35+
36+
dash_duo.start_server(app)
37+
dash_duo.wait_for_text_to_equal("#button1", "Button 1")
38+
assert dash_duo.get_logs() == []
39+
dash_duo.wait_for_text_to_equal("#test-out", "None - None")
40+
dash_duo.find_element("#button1").click()
41+
dash_duo.wait_for_text_to_equal("#test-out", "1 - None")
42+
43+
dash_duo.find_element("#button2").click()
44+
dash_duo.wait_for_text_to_equal("#test-out", "1 - 1")
45+
assert dash_duo.get_logs() == []
46+
47+
48+
def test_cbop002_optional_state(dash_duo):
49+
app = Dash(suppress_callback_exceptions=True)
50+
51+
app.layout = html.Div(
52+
[
53+
html.Button(id="button1", children="Button 1"),
54+
html.Div(id="button-container"),
55+
html.Div(id="test-out"),
56+
html.Div(id="test-out2"),
57+
]
58+
)
59+
60+
@app.callback(
61+
Output("button-container", "children"),
62+
Input("button1", "n_clicks"),
63+
State("button-container", "children"),
64+
prevent_initial_call=True,
65+
)
66+
def _(_, c):
67+
if not c:
68+
return html.Button(id="button2", children="Button 2")
69+
return no_update
70+
71+
@app.callback(
72+
Output("test-out", "children"),
73+
Input("button1", "n_clicks"),
74+
State("button2", "n_clicks", allow_optional=True),
75+
prevent_inital_call=True,
76+
)
77+
def display(n, n2):
78+
return f"{n} - {n2}"
79+
80+
@app.callback(
81+
Output("test-out2", "children"),
82+
Input("button2", "n_clicks", allow_optional=True),
83+
)
84+
def test(n):
85+
if n:
86+
return n
87+
return no_update
88+
89+
dash_duo.start_server(app)
90+
dash_duo.wait_for_text_to_equal("#button1", "Button 1")
91+
assert dash_duo.get_logs() == []
92+
dash_duo.wait_for_text_to_equal("#test-out", "None - None")
93+
dash_duo.find_element("#button1").click()
94+
dash_duo.wait_for_text_to_equal("#test-out", "1 - None")
95+
96+
dash_duo.find_element("#button2").click()
97+
dash_duo.wait_for_text_to_equal("#test-out2", "1")
98+
dash_duo.wait_for_text_to_equal("#test-out", "1 - None")
99+
dash_duo.find_element("#button1").click()
100+
dash_duo.wait_for_text_to_equal("#test-out", "2 - 1")
101+
assert dash_duo.get_logs() == []

0 commit comments

Comments
 (0)