Search code examples
pythonplotlydashboardplotly-dash

Plotly Dash: Dropdown component callback visibility bug


I've been working on an app recently using Dash. The app uses a number of controls, for example Input and Dropdown from the core components. One feature I wanted to include was that only one of the Input or Dropdown is visible at a time, and which one is visible is conditional on what the user chooses (i.e., we use a callback). Here's a minimal example:

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

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

controls = dbc.Card(
    [
        dbc.FormGroup(
            [
               dbc.Label("Choose input or dropdown"),
               dbc.RadioItems(
                   id='radio',
                   options=[
                       {'label': 'input', 'value': 'input'},
                       {'label': 'dropdown', 'value': 'dropdown'},
                   ],
                   value='input',
               ),
            ]
        ),
        dbc.FormGroup(
            [
                dbc.Label("Input", id='input-label'),
                dbc.Input(
                    id='input',
                    type='number',
                    min=1, max=100, step=1, value=5
                ),
            ]
        ),
        dbc.FormGroup(
            [
                dbc.Label("Dropdown", id='dropdown-label'),
                dcc.Dropdown(
                    id='dropdown',
                    options=[
                        {'label': 'Hello'+str(i), 'value': 'Hello'+str(i)} for i in range(100)
                    ],
                    multi=True,
                    placeholder='Say Hello',
                ),
            ]
        ),
    ],
    body=True,
    color='light',
)

app.layout = dbc.Container(
    [
        dbc.Row(
            [
                dbc.Col(controls, md=4)
            ],
            align='center',
        ),
    ],
    fluid=True,
)

@app.callback(
    [Output('input', 'style'),
     Output('input-label', 'style'),
     Output('dropdown', 'style'),
     Output('dropdown-label', 'style')],
    [Input('radio', 'value')])
def visibility(selected_type):
    if selected_type == 'input':
        return {'display': 'block'}, {'display': 'block'}, {'display': 'none'}, {'display': 'none'}
    else:
        return {'display': 'none'}, {'display': 'none'}, {'display': 'block'}, {'display': 'block'}


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

However, using the callback ruins the functionality of the Dropdown. For example, try including 10 or so "Hello's" in the Dropdown. Now, if you comment out the callback in the code, the functionality is restored: adding many "Hello's" causes the Dropdown box to expand to store them, as intended.

Could someone explain why my callback is causing this behaviour?


Solution

  • The issue is that you are directly overwriting the styles of the input component and of the dropdown component in the callback. In order to prevent this issue you can assign an id to their containers, and then change the styles of the containers in the callback. With this approach both the input component and the dropdown component will retain their default behaviour.

    import dash
    import dash_core_components as dcc
    import dash_bootstrap_components as dbc
    from dash.dependencies import Input, Output
    
    app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
    
    controls = dbc.Card(
        [
            dbc.FormGroup(
                [
                   dbc.Label('Choose input or dropdown'),
                   dbc.RadioItems(
                       id='radio',
                       options=[
                           {'label': 'input', 'value': 'input'},
                           {'label': 'dropdown', 'value': 'dropdown'},
                       ],
                       value='input',
                   ),
                ]
            ),
            dbc.FormGroup(id='input-form', children=
                [
                    dbc.Label('Input', id='input-label'),
                    dbc.Input(
                        id='input',
                        type='number',
                        min=1, max=100, step=1, value=5
                    ),
                ]
            ),
            dbc.FormGroup(id='dropdown-form', children=
                [
                    dbc.Label('Dropdown', id='dropdown-label'),
                    dcc.Dropdown(
                        id='dropdown',
                        options=[
                            {'label': 'Hello'+str(i), 'value': 'Hello'+str(i)} for i in range(100)
                        ],
                        multi=True,
                        placeholder='Say Hello',
                    ),
                ]
            ),
        ],
        body=True,
        color='light',
    )
    
    app.layout = dbc.Container(
        [
            dbc.Row(
                [
                    dbc.Col(controls, md=4)
                ],
                align='center',
            ),
        ],
        fluid=True,
    )
    
    @app.callback([Output('input-form', 'style'), Output('dropdown-form', 'style')],
        [Input('radio', 'value')])
    def visibility(selected_type):
    
        if selected_type == 'input':
    
            return {'display': 'block'}, {'display': 'none'}
    
        else:
    
            return {'display': 'none'}, {'display': 'block'}
    
    if __name__ == '__main__':
        app.run_server(debug=True)