diff --git a/CHANGELOG.md b/CHANGELOG.md index 691abbac78..861d68e2ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Fixed - [#2545](https://github.com/plotly/dash/pull/2545) Fix typescript objectOf generation. +- [#2548](https://github.com/plotly/dash/pull/2548) Fix component as props callback triggering other callbacks not in response, fix [#2487](https://github.com/plotly/dash/issues/2487). ## [2.10.0] - 2023-05-25 diff --git a/dash/dash-renderer/src/actions/dependencies_ts.ts b/dash/dash-renderer/src/actions/dependencies_ts.ts index 836c79ac05..9857de1461 100644 --- a/dash/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash/dash-renderer/src/actions/dependencies_ts.ts @@ -1,12 +1,10 @@ import { all, - any, assoc, concat, difference, filter, flatten, - includes, isEmpty, keys, map, @@ -312,13 +310,12 @@ export const getLayoutCallbacks = ( rootId = stringifyId(rootId); // Filter inputs that are not present in the response callbacks = callbacks.filter(cb => - any( - (inp: any) => - !( - stringifyId(inp.id) === rootId && - !includes(inp.property, options.filterRoot) - ), - cb.callback.inputs + cb.callback.inputs.reduce( + (previous: any, input: any) => + previous || + (stringifyId(input.id) == rootId && + options.filterRoot.includes(input.property)), + false ) ); } diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py index b1f3324cb4..fa86aa32c5 100644 --- a/tests/integration/renderer/test_component_as_prop.py +++ b/tests/integration/renderer/test_component_as_prop.py @@ -4,7 +4,7 @@ from dash_test_components import ComponentAsProp -from dash.dcc import Checklist +from dash.dcc import Checklist, Dropdown from dash.html import Button, Div, Span @@ -379,3 +379,65 @@ def opts(n): dash_duo.find_elements("#b label > input")[0].click() dash_duo.wait_for_text_to_equal("#counter", "1") + + +def test_rdcap004_side_effect_same_component(dash_duo): + options = [ + {"label": "aa1", "value": "aa1"}, + {"label": "aa2", "value": "aa2"}, + {"label": "aa3", "value": "aa3"}, + {"label": "best value", "value": "bb1"}, + {"label": "better value", "value": "bb2"}, + {"label": "bye", "value": "bb3"}, + ] + + app = Dash(__name__) + + app.layout = Div( + [ + Div( + ["Single dynamic Dropdown", Dropdown(id="my-dynamic-dropdown")], + style={"width": 200, "marginLeft": 20, "marginTop": 20}, + ), + Button( + "Reset", + id="button", + n_clicks=0, + ), + Div(0, id="counter"), + ] + ) + app.clientside_callback( + "function(_, prev) {return parseInt(prev) + 1}", + Output("counter", "children"), + Input("my-dynamic-dropdown", "value"), + State("counter", "children"), + prevent_initial_call=True, + ) + + @app.callback( + Output("my-dynamic-dropdown", "options"), + Input("my-dynamic-dropdown", "search_value"), + ) + def update_options(search_value): + if search_value is None: + return options + return [o for o in options if search_value in o["label"]] + + @app.callback( + Output("my-dynamic-dropdown", "value"), + Input("button", "n_clicks"), + ) + def on_button(n_clicks): + return None + + dash_duo.start_server(app) + + # Initial callback + dash_duo.wait_for_text_to_equal("#counter", "1") + + search = dash_duo.wait_for_element("#my-dynamic-dropdown input") + + search.send_keys("a") + + dash_duo.wait_for_text_to_equal("#counter", "1")