Dynamically update dropdown options/menu list without clearing previously selected options

I’m updating options for dropdown via a callback, as soon as any item is selected from dropdown menu list, a new option list is returned to the same dropdown but it clears the items that were previously selected.

Q) How do I update the dropdown option/list without clearing the items that previously selected ?

In the MRE posted below , dropdown is first populated with ‘option_1’ list and then upon selection of any value, a new list is passed ‘option_2’ . Since multi is set to TRUE , the item(s) selected from ‘option_1’ list is cleared and dropdown menu/list is updated as expected but I want items that were previously selected (from ‘option_1’) to also be there.

from dash import Dash, dcc, html, dcc, Input, Output, State, callback
from dash.exceptions import PreventUpdate

option_1 = [
    {"label": "Top level", "value": "TP"},
    {"label": "Top level 2", "value": "TP2"},
    {"label": "Top level 3", "value": "TP3"},
]
option_2 = [
    {"label": "options1", "value": "op1"},
    {"label": "options2", "value": "op2"},
    {"label": "options3", "value": "op3"},
]
app = Dash(__name__)
app.layout = html.Div([
    html.Div([
        "Dynamic Dropdown",
        dcc.Dropdown(id="dyn-dropdown", 
                    multi=True,
                    searchable=False,
                    options=option_1),
    ]),
])


@callback(
    Output("dyn-dropdown", "options"),
    Input("dyn-dropdown", "value"),
    prevent_initial_call=True
)
def update_multi_options(value):
    if not value:
        raise PreventUpdate
    return option_2

if __name__ == "__main__":
    app.run(debug=True)

As previously described, the gif demonstrates that ‘Top level’ option is cleared as soon as option list is updated via callback, but I want ‘top level’ option to be there as well.

What I want is to create a sort of ‘filtered’ menu list, where upon selecting a single item in dropdown menu, it updates the same dropdown with new options while also keeping the previously selected options.

dyn_dropdown

Any assistance would be appreciated please.

This might be innteresting:

I dont see how I can make use of this.I dont have enough space in my UI design to fit a checklist to filter through my options. A dynamically filtered dropdown is what I need.

Hello @ptser,

Give this a test and see if it works for you:

from dash import Dash, dcc, html, dcc, Input, Output, State, callback
from dash.exceptions import PreventUpdate

option_1 = [
    {"label": "Top level", "value": "TP"},
    {"label": "Top level 2", "value": "TP2"},
    {"label": "Top level 3", "value": "TP3"},
]
option_2 = [
    {"label": "options1", "value": "op1"},
    {"label": "options2", "value": "op2"},
    {"label": "options3", "value": "op3"},
]
app = Dash(__name__)
app.layout = html.Div([
    html.Div([
        "Dynamic Dropdown",
        dcc.Dropdown(id="dyn-dropdown",
                    multi=True,
                    searchable=False,
                    options=option_1),
    ]),
])


@callback(
    Output("dyn-dropdown", "options"),
    Input("dyn-dropdown", "value"),
    State("dyn-dropdown", "options"),
    prevent_initial_call=True
)
def update_multi_options(values, opts):
    if not values:
        return option_1
    opts1 = [v['value'] for v in option_1]
    opts2 = [v['value'] for v in option_2]
    opt1Mapped = {v['value']:v for v in option_1}
    if values[0] not in opts1:
        return option_1
    for v in values:
        if v in opts2:
            raise PreventUpdate
    return [opt1Mapped[values[0]]]+option_2

if __name__ == "__main__":
    app.run(debug=True)

If the first selected value doesnt exist in the first set of options, it resets the options to options1, otherwise it adds the selected top level to the options of options2.

3 Likes

This works perfectly. @jinnyzor Thank you.

Would you care to answer a few questions ?

If i understand this correctly, we are returning the first selected option (in our case its {‘label’: ‘Top level’, ‘value’: ‘TP’}) + new options list (i.e. option_2 list). If this is true, then how does the dropdown component “know” that Top Level was selected previously and shows it as an selected option instead of showing it in dropdown menu/list ?

Further more, if i need to extend this such that by selecting ‘option 1’ , a new dropdown menu is shown, would I need to return ‘Top level’ + ‘option 1’ + new_option_list ?

If i understand this correctly, we are returning the first selected option (in our case its {‘label’: ‘Top level’, ‘value’: ‘TP’}) + new options list (i.e. option_2 list). If this is true, then how does the dropdown component “know” that Top Level was selected previously and shows it as an selected option instead of showing it in dropdown menu/list ?

This is based upon the very first value in the list is generated from options1. If the value is not in list one, then it resets.

Further more, if i need to extend this such that by selecting ‘option 1’ , a new dropdown menu is shown, would I need to return ‘Top level’ + ‘option 1’ + new_option_list ?

Yes, this is possible. :smiley:

1 Like

This is just a fun example of how you could build it to be completely dynamic. :wink:

from dash import Dash, dcc, html, dcc, Input, Output, State, callback
from dash.exceptions import PreventUpdate

options = {f'{x}': [{'label': f'{x}_{y}', 'value': f'{x}_{y}'} for y in range(5)] for x in ['one', 'two', '3', 'four', 'five']}

keys = list(options.keys())

app = Dash(__name__)
app.layout = html.Div([
    html.Div([
        "Dynamic Dropdown",
        dcc.Dropdown(id="dyn-dropdown",
                    multi=True,
                    searchable=False,
                    options=options[keys[0]]),
    ]),
])


@callback(
    Output("dyn-dropdown", "options"),
    Input("dyn-dropdown", "value"),
    prevent_initial_call=True
)
def update_multi_options(values):
    if not values:
        return options[keys[0]]
    newOpts = []
    for x in range(len(options)+1):
        if x == len(values) or x == len(options):
            if x < len(options):
                newOpts += options[keys[x]]
            else:
                newOpts += options[keys[x-1]]
            break
        if keys[x] == values[x].split('_')[0]:
            optMapped = {f'{y["value"]}': y for y in options[keys[x]]}
            newOpts.append(optMapped[values[x]])
        else:
            newOpts += options[keys[x]]
            break
    return newOpts

if __name__ == "__main__":
    app.run(debug=True)
3 Likes