Black Lives Matter. Please consider donating to Black Girls Code today.
Dash HoloViews is now available! Check out the docs.

Switch values button + parent-child (dropdowns) relation impossible

Hi all,

I have a case with elements A, B, C where:

  • A -> B
  • B -> C
  • A -> C (with priority over B -> C)

Do you think it is possible such relations with callbacks? I did not find a solution because:

  • In one callback, it would raise a dash.exceptions.SameInputOutputException as B would be as Input and Output of the callback.
  • In two callbacks, it would raise a dash.exceptions.DuplicateCallbackOutput as C would be as Output of the two callbacks.

Here is my practical case. I have two parents (dropdowns) having each a child (dropdown). When we modify a parent value, the options (and the value) of its child are updated. My problem arises when I want to add a button to be able to switch the dropdowns values (both parents and children), i.e. the second callback is executed and the children values are set to default, while I would like to keep the original values of the children.

Here is the current code:


#################################################################################
# DEFINITIONS
#################################################################################

OPTIONS_DIC = {
    'A': [
        {'label': '1st option of A', 'value': 'A1'},
        {'label': '2nd option of A', 'value': 'A2'}
    ],
    'B': [
        {'label': '1st option of B', 'value': 'B1'},
        {'label': '2nd option of B', 'value': 'B2'}
    ],
    'C': [
        {'label': '1st option of C', 'value': 'C1'},
        {'label': '2nd option of C', 'value': 'C2'}
    ],
    'D': [
        {'label': '1st option of D', 'value': 'D1'},
        {'label': '2nd option of D', 'value': 'D2'}
    ],
}

#################################################################################
# Application Layout
#################################################################################

app = dash.Dash(__name__)
app.layout = html.Div(
    [
        html.Button('Switch', id='switch-button', n_clicks=0),
        dcc.Dropdown(
            id='parent-1',
            options=[{'label': key, 'value': key} for key in OPTIONS_DIC],
            value='A'
        ),
        dcc.Dropdown(id='child-1'),
        dcc.Dropdown(
            id='parent-2',
            options=[{'label': key, 'value': key} for key in OPTIONS_DIC],
            value='C'
        ),
        dcc.Dropdown(id='child-2')
    ]
)

#################################################################################
# Callbacks
#################################################################################

@app.callback(
    [Output('parent-1', 'value'), Output('parent-2', 'value')],
    [Input('switch-button', 'n_clicks')],
    [State('parent-1', 'value'), State('parent-2', 'value')],
)
def switch_parents(n_clicks, parent_1, parent_2):
    if n_clicks == 0:
        raise PreventUpdate
    return parent_2, parent_1

for n in ['1', '2']:
    @app.callback(
        [Output('child-' + n, 'value'), Output('child-' + n, 'options')],
        [Input('parent-' + n, 'value')],
    )
    def update_children(parent):
        return OPTIONS_DIC[parent][0]['value'], OPTIONS_DIC[parent]

Hi @younos, welcome to the forum! Below is an example which uses a dcc.Store to store the last child value for each of the possible parents values, and then you can use it to restore the last used value when a new parent value is chosen. Hope it helps!

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


app = dash.Dash(__name__)

#################################################################################
# DEFINITIONS
#################################################################################

OPTIONS_DIC = {
    'A': [
        {'label': '1st option of A', 'value': 'A1'},
        {'label': '2nd option of A', 'value': 'A2'}
    ],
    'B': [
        {'label': '1st option of B', 'value': 'B1'},
        {'label': '2nd option of B', 'value': 'B2'}
    ],
    'C': [
        {'label': '1st option of C', 'value': 'C1'},
        {'label': '2nd option of C', 'value': 'C2'}
    ],
    'D': [
        {'label': '1st option of D', 'value': 'D1'},
        {'label': '2nd option of D', 'value': 'D2'}
    ],
}

#################################################################################
# Application Layout
#################################################################################

app.layout = html.Div(
    [
        html.Button('Switch', id='switch-button', n_clicks=0),
        dcc.Dropdown(
            id='parent-1',
            options=[{'label': key, 'value': key} for key in OPTIONS_DIC],
            value='A'
        ),
        dcc.Dropdown(id='child-1'),
        dcc.Dropdown(
            id='parent-2',
            options=[{'label': key, 'value': key} for key in OPTIONS_DIC],
            value='C'
        ),
        dcc.Dropdown(id='child-2'),
        dcc.Store(id='store', data=dict(A='A1', B='C2', C='C1', D='D2'))
    ]
)

#################################################################################
# Callbacks
#################################################################################

@app.callback(
    [Output('parent-1', 'value'), Output('parent-2', 'value')],
    [Input('switch-button', 'n_clicks')],
    [State('parent-1', 'value'), State('parent-2', 'value')],
)
def switch_parents(n_clicks, parent_1, parent_2):
    if n_clicks == 0:
        return dash.no_update, dash.no_update
    return parent_2, parent_1

for n in ['1', '2']:
    @app.callback(
        [Output('child-' + n, 'value'), Output('child-' + n, 'options')],
        [Input('parent-' + n, 'value')],
        [State('store', 'data')]
    )
    def update_children(parent, store_values):
        return store_values[parent], OPTIONS_DIC[parent]


@app.callback(
    Output('store', 'data'),
    [Input('child-1', 'value'),
     Input('child-2', 'value')],
    [State('parent-1', 'value'),
     State('parent-2', 'value'),
     State('store', 'data')])
def update_store(child_1, child_2, parent_1, parent_2, store_values):
    if child_1 is None or child_2 is None:
        return dash.no_update
    store_values[parent_1] = child_1
    store_values[parent_2] = child_2
    return store_values


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

Thanks a lot @Emmanuelle. This solved my problem!