Skip to content

Commit b8a993e

Browse files
authored
Merge pull request #2429 from plotly/fix-2411
Fix side effect on updating possible array children triggering callbacks that were not updated.
2 parents 3243512 + 72f290f commit b8a993e

File tree

4 files changed

+65
-4
lines changed

4 files changed

+65
-4
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1313

1414
## Fixed
1515

16+
- [#2429](https://github.com/plotly/dash/pull/2429) Fix side effect on updating possible array children triggering callbacks, fix [#2411](https://github.com/plotly/dash/issues/2411).
1617
- [#2417](https://github.com/plotly/dash/pull/2417) Disable the pytest plugin if `dash[testing]` not installed, fix [#946](https://github.com/plotly/dash/issues/946).
1718
- [#2417](https://github.com/plotly/dash/pull/2417) Do not swallow the original error to get the webdriver, easier to know what is wrong after updating the browser but the driver.
1819
- [#2425](https://github.com/plotly/dash/pull/2425) Fix multiple log handler added unconditionally to the logger, resulting in duplicate log message.

Diff for: dash/dash-renderer/src/actions/dependencies_ts.ts

+21
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import {
22
all,
3+
any,
34
assoc,
45
concat,
56
difference,
67
filter,
78
flatten,
89
forEach,
10+
includes,
911
isEmpty,
1012
keys,
1113
map,
1214
mergeWith,
1315
partition,
16+
path,
1417
pickBy,
1518
props,
1619
reduce,
@@ -304,6 +307,24 @@ export const getLayoutCallbacks = (
304307
);
305308
}
306309

310+
if (options.filterRoot) {
311+
let rootId = path(['props', 'id'], layout);
312+
if (rootId) {
313+
rootId = stringifyId(rootId);
314+
// Filter inputs that are not present in the response
315+
callbacks = callbacks.filter(cb =>
316+
any(
317+
(inp: any) =>
318+
!(
319+
stringifyId(inp.id) === rootId &&
320+
!includes(inp.property, options.filterRoot)
321+
),
322+
cb.callback.inputs
323+
)
324+
);
325+
}
326+
}
327+
307328
/*
308329
Return all callbacks with an `executionGroup` to allow group-processing
309330
*/

Diff for: dash/dash-renderer/src/observers/executedCallbacks.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ const observer: IStoreObserverDefinition<IStoreState> = {
138138
const handlePaths = (
139139
children: any,
140140
oldChildren: any,
141-
oldChildrenPath: any[]
141+
oldChildrenPath: any[],
142+
filterRoot: any = false
142143
) => {
143144
const oPaths = getState().paths;
144145
const paths = computePaths(
@@ -152,7 +153,8 @@ const observer: IStoreObserverDefinition<IStoreState> = {
152153
requestedCallbacks = concat(
153154
requestedCallbacks,
154155
getLayoutCallbacks(graphs, paths, children, {
155-
chunkPath: oldChildrenPath
156+
chunkPath: oldChildrenPath,
157+
filterRoot
156158
}).map(rcb => ({
157159
...rcb,
158160
predecessors
@@ -166,7 +168,8 @@ const observer: IStoreObserverDefinition<IStoreState> = {
166168
getLayoutCallbacks(graphs, oldPaths, oldChildren, {
167169
removedArrayInputsOnly: true,
168170
newPaths: paths,
169-
chunkPath: oldChildrenPath
171+
chunkPath: oldChildrenPath,
172+
filterRoot
170173
}).map(rcb => ({
171174
...rcb,
172175
predecessors
@@ -204,7 +207,8 @@ const observer: IStoreObserverDefinition<IStoreState> = {
204207
}
205208
},
206209
oldObj,
207-
basePath
210+
basePath,
211+
keys(appliedProps)
208212
);
209213
// Only do it once for the component.
210214
recomputed = true;

Diff for: tests/integration/renderer/test_component_as_prop.py

+35
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,38 @@ def demo(n_clicks):
258258
dash_duo.wait_for_text_to_equal(f"#options label:nth-child({i}) span", "")
259259
dash_duo.wait_for_element(f"#options label:nth-child({i}) button").click()
260260
dash_duo.wait_for_text_to_equal(f"#options label:nth-child({i}) span", "1")
261+
262+
263+
def test_rdcap003_side_effect_regression(dash_duo):
264+
# Test for #2411, regression introduced by original rdcap002 fix
265+
# callback on the same components that is output with same id but not property triggered
266+
# on cap components of array type like Checklist.options[] and Dropdown.options[].
267+
app = Dash(__name__)
268+
269+
app.layout = Div([Button("3<->2", id="a"), Checklist(id="b"), Div(0, id="counter")])
270+
271+
app.clientside_callback(
272+
"function(_, prev) {return parseInt(prev) + 1}",
273+
Output("counter", "children"),
274+
Input("b", "value"),
275+
State("counter", "children"),
276+
prevent_initial_call=True,
277+
)
278+
279+
@app.callback(Output("b", "options"), Input("a", "n_clicks"))
280+
def opts(n):
281+
n_out = 3 - (n or 0) % 2
282+
return [str(i) for i in range(n_out)]
283+
284+
dash_duo.start_server(app)
285+
286+
dash_duo.wait_for_text_to_equal("#counter", "0")
287+
dash_duo.find_element("#a").click()
288+
assert len(dash_duo.find_elements("#b label > input")) == 2
289+
dash_duo.wait_for_text_to_equal("#counter", "0")
290+
dash_duo.find_element("#a").click()
291+
assert len(dash_duo.find_elements("#b label > input")) == 3
292+
dash_duo.wait_for_text_to_equal("#counter", "0")
293+
294+
dash_duo.find_elements("#b label > input")[0].click()
295+
dash_duo.wait_for_text_to_equal("#counter", "1")

0 commit comments

Comments
 (0)