Search code examples
pythoncallbackdropdownplotly-dash

Python Dash: How to use Input from a dynamically created Dropdown?


I got an app that contains a button with callback function to create an unlimited amount of dropdowns which will be automatically id'ed as 'dropdown-i'. The struggle is that I don't seem to be able to actually use the values I input in these Dropdowns in another callback function (that's only trying to print them).

How can I retrieve these values or how would you do this?

Apparently the part value=dcc.Dropdown(id=dropdown_id).value doesn't work.

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)

app.layout = html.Div([
    html.Button("Add Dropdown and Number Field", id="add-button"),
    html.Div(id="input-container", children=[]),
    html.Div(id="output"), 
])

@app.callback(
    Output("input-container", "children"),
    Input("add-button", "n_clicks"),
    State("input-container", "children")
)
def add_input(n_clicks, existing_children):
    if n_clicks is None:
        return existing_children
    
    new_input = dbc.Row([
        dbc.Col(dcc.Dropdown(
            options=[
                {'label': 'Option 1', 'value': 'option-1'},
                {'label': 'Option 2', 'value': 'option-2'},
                # Add more dropdown options as needed
            ],
            value='option-1',
            id=f'dropdown-{n_clicks}'
        )),
        dbc.Col(dcc.Input(
            type='number',
            value=0,
            id=f'weight-{n_clicks}'
        )),
    ])
    
    existing_children.append(new_input)
    
    return existing_children

@app.callback(
    Output("output", "children"),
    Input("add-button", "n_clicks"),
    State("input-container", "children")
)
def process_dropdowns(n_clicks, dropdown_children):
    if n_clicks is None:
        return []

    # Create a list to store the selected values from each dropdown
    selected_values = []

    # Iterate through the dropdowns to retrieve their values
    for i, child in enumerate(dropdown_children):
        dropdown_id = f'dropdown-{i+1}'
        selected_value = dcc.Dropdown(id=dropdown_id).value
        selected_values.append(selected_value)

    # Process the selected values or use them as needed
    return f"Selected Dropdown Values: {', '.join(selected_values)}"

if __name__ == "__main__":
    app.run_server(debug=False)

Solution

  • This is the typical use case for leveraging pattern-matching callback selectors.

    The pattern-matching callback selectors MATCH, ALL, & ALLSMALLER allow you to write callbacks that respond to or update an arbitrary or dynamic number of components.

    The idea is to use composite id's (type+index) using dictionaries rather than strings, so that we can identify a given component as being the nth component of a specific type.

    I also updated the first callback so that it makes partial property updates rather than sending back and forth all data across the network, which makes it more efficient.

    from dash import Dash, dcc, html, Input, Output, ALL, Patch, callback, no_update
    import dash_bootstrap_components as dbc
    
    app = Dash(__name__)
    
    app.layout = html.Div([
        html.Button("Add Dropdown and Number Field", id="add-button"),
        html.Div(id="input-container", children=[]),
        html.Div(id="output"),
    ])
    
    
    @app.callback(
        Output("input-container", "children"),
        Input("add-button", "n_clicks")
    )
    def add_input(n_clicks):
        if n_clicks is None:
            return no_update
    
        patched_children = Patch()
    
        new_input = dbc.Row([
            dbc.Col(dcc.Dropdown(
                id={'type': 'dropdown', 'index': n_clicks},
                options=[
                    {'label': 'Option 1', 'value': 'option-1'},
                    {'label': 'Option 2', 'value': 'option-2'},
                    # Add more dropdown options as needed
                ],
                value='option-1',
            )),
            dbc.Col(dcc.Input(
                id={'type': 'weight', 'index': n_clicks},
                type='number',
                value=0,
            )),
        ])
    
        patched_children.append(new_input)
        return patched_children
    
    
    @callback(
        Output("output", "children"),
        Input({"type": "dropdown", "index": ALL}, "value"),
    )
    def process_dropdowns(values):
        return html.Div(
            ['Selected Dropdown Values:'] +
            [html.Div(f"Dropdown {i + 1} = {value}") for (i, value) in enumerate(values)]
        )
    
    if __name__ == "__main__":
        app.run_server(debug=False)