I have a multi-select dropdown with many options. I want to limit the maximum number of values a user can select to 3 but I don’t want to disable the dropdown at 3 - I want the user to be able to remove values and add new ones once they reach 3.
I tried writing a callback with the dropdown values as both Input and Output. I was hoping that I could check if len(dropdown_values) > 3 then I would return dropdown_values[0:3] for the values. But it seems the same component cannot be both an Input and an Output.
Any advice on how I can achieve this behavior would be greatly appreciated.
That’s a good question, sorry I forgot to include that part after I discovered it.
Yes, there is circularity in the above: comparison-dropdown.value [select_comparison()] > store-prev-comparisons.data [trim_dropdown()] > comparison-dropdown.value [select_comparison()].
The way to stop it is to observe that after trim_dropdown() finishes you have comparison-dropdown.value == store-prev-comparisons.data. So all you have to do is pass store-prev-comparisons.data as a State() to select_comparison(), check for this equality, and PreventUpdate if it holds.
@app.callback([Output('store-prev-comparisons', 'data')],
[Input('comparison-dropdown', 'value')],
[State('store-prev-comparisons', 'data')])
def select_comparison(comparisons, prev_comparisons):
if len(comparisons) == 4:
# changes store-prev-comparisons which triggers above callback
return comparisons[0:3],
elif comparisons == prev_comparisons:
# this only happens if we just trimmed so don't do anything to break circularity
raise dash.exception.PreventUpdate
else:
# when <= 3 don't modify store-prev-comparisons and therefore don't trigger above
return dash.no_update
By the way, if you have other callbacks that respond to comparison-dropdown.value to do something with the values you can add the following to keep them from responding when the limit is reached and the above trimming executes.
if len(comparisons) == MAX_COMPARISONS + 1 or comparisons == prev_comparisons:
raise dash.exceptions.PreventUpdate
Greetings, I’ve stumbled upon this thread and realized how badly I need something like this in my dashboard.
However I’m struggling trying to implement your syntax into mine.
Here is my dropdown callback, hopefully someone is able to help me solve the same issue you had.
@app.callback(
dash.dependencies.Output("input-warning", "children"),
[dash.dependencies.Input("dynamic-dropdown", "search_value")],
[dash.dependencies.State("dynamic-dropdown", "value")],
)
def update_multi_options(search_value, value):
conn.rollback()
if search_value is None:
return [[], OPTIONS]
elif len(search_value) >= 4:
children = html.P("limit reached",id='input-warning')
return [children,[option for option in OPTIONS if option['value'] in search_value]]
else:
# Make sure that the set values are in the option list, else they will disappear
# from the shown select list, but still part of the `value`.
return [
o for o in OPTIONS if search_value.upper() in o["label"].upper() or o["value"] in (value or [])
]
I found another solution that works pretty well for this - in a callback that fires on the dcc.Dropdown value, if the len(value) reaches my limit, I disable all the options in the dcc.Dropdown. That way, they can still remove items to get below the limit, but they are prevented from adding more.
Thanks for the tip @swt2c. I got this working. I’ve shared the code for my callback in case anyone else wants to use this technique.
The dropdown menu is generated from the columns of a table in my example when the user clicks a button.
Once the user has selected three options, the rest of the dropdown menu options are disabled. Deleting some of the selected values or clearing the menu re-enables the options.
@app.callback(Output('sensor-dropdown', 'options'),
[Input('sensor-data-button','n_clicks'),
Input('sensor-dropdown', 'value')],
[State('input-sensor-table', 'columns'),
State('sensor-dropdown', 'options')],
prevent_initial_callbacks=True)
def sensorSelect(sensorButton, sensorsSelected, sensorCols, sensorOptions):
if not sensorCols:
raise PreventUpdate()
sensorSampleSelect = []
if sensorOptions is None or sensorsSelected is None:
sensorSampleSelect = [{'label':i['name'],'value':i['name'], 'disabled': False} for i in sensorCols]
return sensorSampleSelect
elif len(sensorsSelected) < 3:
sensorSampleSelect = [{'label':i['name'],'value':i['name'], 'disabled': False} for i in sensorCols]
return sensorSampleSelect
elif len(sensorsSelected) >= 3:
sensorSampleSelect = [{'label':i['name'],'value':i['name'], 'disabled': True} for i in sensorCols]
return sensorSampleSelect