From 024d936a9b4959aee159611bb8109ee1e77c4aa9 Mon Sep 17 00:00:00 2001 From: Tyler Baur Date: Tue, 26 Mar 2024 11:06:10 -0500 Subject: [PATCH 1/3] fix searchable dropdown option removal. track search_value, instead of searchable --- .../src/fragments/Dropdown.react.js | 12 ++++++------ .../tests/integration/dropdown/test_remove_option.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 0464fb8ff4..c8c6ce2207 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -39,7 +39,6 @@ const Dropdown = props => { const { id, clearable, - searchable, multi, options, setProps, @@ -48,6 +47,7 @@ const Dropdown = props => { value, } = props; const [optionsCheck, setOptionsCheck] = useState(null); + const [searchValue, setSearchValue] = useState(null); const persistentOptions = useRef(null); if (!persistentOptions || !isEqual(options, persistentOptions.current)) { @@ -115,14 +115,14 @@ const Dropdown = props => { [multi] ); - const onInputChange = useCallback( - search_value => setProps({search_value}), - [] - ); + const onInputChange = useCallback(search_value => { + setProps({search_value}); + setSearchValue(search_value); + }, []); useEffect(() => { if ( - !searchable && + !searchValue && !isNil(sanitizedOptions) && optionsCheck !== sanitizedOptions && !isNil(value) diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py index 6986380b45..4e26b130cd 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py +++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py @@ -1,5 +1,7 @@ import json +import pytest + from dash import Dash, html, dcc, Output, Input from dash.exceptions import PreventUpdate @@ -11,7 +13,8 @@ ] -def test_ddro001_remove_option_single(dash_dcc): +@pytest.mark.parametrize("searchable", (True, False)) +def test_ddro001_remove_option_single(dash_dcc, searchable): dropdown_options = sample_dropdown_options app = Dash(__name__) @@ -22,7 +25,7 @@ def test_ddro001_remove_option_single(dash_dcc): dcc.Dropdown( options=dropdown_options, value=value, - searchable=False, + searchable=searchable, id="dropdown", ), html.Button("Remove option", id="remove"), @@ -49,7 +52,8 @@ def on_change(val): dash_dcc.wait_for_text_to_equal("#value-output", "None") -def test_ddro002_remove_option_multi(dash_dcc): +@pytest.mark.parametrize("searchable", (True, False)) +def test_ddro002_remove_option_multi(dash_dcc, searchable): dropdown_options = sample_dropdown_options app = Dash(__name__) @@ -62,7 +66,7 @@ def test_ddro002_remove_option_multi(dash_dcc): value=value, multi=True, id="dropdown", - searchable=False, + searchable=searchable, ), html.Button("Remove option", id="remove"), html.Div(id="value-output"), From dc5668228afd2411b4dda2092cfa6555f1cd0706 Mon Sep 17 00:00:00 2001 From: Tyler Baur Date: Wed, 27 Mar 2024 10:42:01 -0500 Subject: [PATCH 2/3] remove state and use `search_value` instead. add test for multiple dropdowns --- .../src/fragments/Dropdown.react.js | 12 ++-- .../dropdown/test_remove_option.py | 63 ++++++++++++++++++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index c8c6ce2207..6847be025d 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -42,12 +42,12 @@ const Dropdown = props => { multi, options, setProps, + search_value, style, loading_state, value, } = props; const [optionsCheck, setOptionsCheck] = useState(null); - const [searchValue, setSearchValue] = useState(null); const persistentOptions = useRef(null); if (!persistentOptions || !isEqual(options, persistentOptions.current)) { @@ -115,14 +115,14 @@ const Dropdown = props => { [multi] ); - const onInputChange = useCallback(search_value => { - setProps({search_value}); - setSearchValue(search_value); - }, []); + const onInputChange = useCallback( + search_value => setProps({search_value}), + [] + ); useEffect(() => { if ( - !searchValue && + !search_value && !isNil(sanitizedOptions) && optionsCheck !== sanitizedOptions && !isNil(value) diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py index 4e26b130cd..116fae135f 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py +++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py @@ -2,7 +2,7 @@ import pytest -from dash import Dash, html, dcc, Output, Input +from dash import Dash, html, dcc, Output, Input, State from dash.exceptions import PreventUpdate @@ -88,3 +88,64 @@ def on_change(val): btn.click() dash_dcc.wait_for_text_to_equal("#value-output", '["MTL"]') + + +def test_ddro003_remove_option_multiple_dropdowns(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="available-options", + multi=True, + options=sample_dropdown_options, + value=["MTL", "NYC", "SF"], + ), + dcc.Dropdown( + id="chosen", + multi=True, + options=sample_dropdown_options, + value=["NYC", "SF"], + ), + html.Button(id="remove-btn", children="Remove"), + html.Button(id="submit-btn", children="Submit"), + html.Div(id="value-output"), + html.Div(id="options-output"), + ], + ) + + @app.callback( + Output("chosen", "options"), + Input("available-options", "value"), + ) + def update_options(available_options): + if available_options is None: + return [] + else: + return [{"label": i, "value": i} for i in available_options] + + @app.callback( + Output("available-options", "options"), [Input("remove-btn", "n_clicks")] + ) + def on_click(n_clicks): + if not n_clicks: + raise PreventUpdate + return sample_dropdown_options[:-1] + + @app.callback( + [Output("value-output", "children"), Output("options-output", "children")], + Input("submit-btn", "n_clicks"), + State("chosen", "options"), + State("chosen", "value"), + ) + def print_value(n_clicks, options, value): + if not n_clicks: + raise PreventUpdate + return [json.dumps(value), json.dumps([i["value"] for i in options])] + + dash_dcc.start_server(app) + btn = dash_dcc.wait_for_element("#remove-btn") + btn.click() + btn = dash_dcc.wait_for_element("#submit-btn") + btn.click() + dash_dcc.wait_for_text_to_equal("#value-output", '["NYC"]') + dash_dcc.wait_for_text_to_equal("#options-output", '["MTL", "NYC"]') From 17b477b873daa5b9e8b59aeab2eaad958b73b48e Mon Sep 17 00:00:00 2001 From: Tyler Baur Date: Wed, 27 Mar 2024 13:26:19 -0500 Subject: [PATCH 3/3] remove guard clause to prevent update. The callback should always update --- .../tests/integration/dropdown/test_remove_option.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py index 116fae135f..a90ffe400c 100644 --- a/components/dash-core-components/tests/integration/dropdown/test_remove_option.py +++ b/components/dash-core-components/tests/integration/dropdown/test_remove_option.py @@ -41,15 +41,13 @@ def on_click(n_clicks): @app.callback(Output("value-output", "children"), [Input("dropdown", "value")]) def on_change(val): - if not val: - raise PreventUpdate - return val or "None" + return val or "Nothing Here" dash_dcc.start_server(app) btn = dash_dcc.wait_for_element("#remove") btn.click() - dash_dcc.wait_for_text_to_equal("#value-output", "None") + dash_dcc.wait_for_text_to_equal("#value-output", "Nothing Here") @pytest.mark.parametrize("searchable", (True, False))