Dash modal with separate button callbacks

In Python I am working with Dash and Dash bootstrap components. I am having trouble with the bootstrap Modal component. My most basic implementation of the modal, using the first example here, works fine. However, If I try to replace their single callback with two separate callbacks for the buttons, it stops working. Can anybody explain to me why this is and help me get it working? The callbacks don’t seem difficult at all.

Most basic implementation of their code (this works for me):

from dash import Dash, html
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
from dash_bootstrap_components.themes import LUMEN

app = Dash(external_stylesheets=[LUMEN])
app.title = "Test"
app.layout = html.Div(
    [
        dbc.Button("Open modal", id="open", n_clicks=0),
        dbc.Modal(
            [
                dbc.ModalHeader(dbc.ModalTitle("Header")),
                dbc.ModalBody("This is the content of the modal"),
                dbc.ModalFooter(
                    dbc.Button(
                        "Close", id="close", className="ms-auto", n_clicks=0
                    )
                ),
            ],
            id="modal",
            is_open=False,
        ),
    ]
)

@app.callback(
    Output("modal", "is_open"),
    [Input("open", "n_clicks"), Input("close", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    print(is_open)
    print(type(is_open))
    if n1 or n2:
        return not is_open
    return is_open

app.run() 

Now I’ve replaced the callback above with the following two callbacks, which breaks

@app.callback(
    Output("modal", "is_open"),
    Input("open", "n_clicks")
)
def toggle_modal_open(n1):
    if n1:
        return True

@app.callback(
    Output("modal", "is_open"),
    Input("close", "n_clicks")
)
def toggle_modal_close(n1):
    if n1:
        return False

HI @Noone234 welcome to the forums.

Reason for this is that you try to update the same component property from different callbacks.

You have two options:

For your application I would recommend the first approach.

Thank you for the quick clear answer! Is that a rule that holds for all components properties? I didn’t know it yet.

I’m still not sure how to solve my problem then. Let me elaborate. I want to have an ‘Add data’ button that opens a form where the user can input data. When the user clicks the ‘OK’ button inside the form, the form should be closed, and some Storage component should be updated. However, if the data is incorrect, the modal should remain open and the storage object should not be updated (and probably some component inside the modal needs to be updated, but let’s ignore that for now).

Now to open/close the modal with the two buttons, they need to be in the same callback. If I also want to update my storage component in that callback, it looks a bit like this:

[Output("modal", "is_open"), Output("Storage component", "data")]
[Input("Add_data_button","n_clicks"), Input("OK_button","n_clicks")]
[State("modal", "is_open"), State("some_modal_input_field", "value")]

I don’t think this works, because don’t want to update my storage component when the “Add_data_button” is clicked. However, if I want to update my storage component in a separate callback:

[Output("Storage component", "data")]
[Input("OK_button","n_clicks")]
[State("some_modal_input_field", "value")]

That doesn’t seem very good either. Both my callbacks now need to check if the input is correct (The open/close modal callback to see if it should close the modal, and this storage callback to see if it should add the data)

I think there must be a nicer option which I am missing?

yes. This is one of the weak points of dash IMHO. Callbacks can get quite messy if you have several components which update a single component.property.

You will have to come up with a logic which fits your needs. I responded here to a related question:

1 Like

You could disable the OK button if the data is incorrect

If you need further assistance, let us know.

I played around with this a while ago and found the code snippet today. Instead of deleting it, I leave it here :wink:

import dash_bootstrap_components as dbc
from dash import Input, Output, State, html, dcc, ctx
import dash

# design of the modal
modal = html.Div(
    [
        dbc.Modal(
            [
                dbc.ModalHeader(dbc.ModalTitle("Header")),
                dbc.ModalBody("Confirm or cancel"),
                dbc.ModalFooter(
                    children=[
                        dbc.ButtonGroup(
                            [
                                dbc.Button(
                                    "OK",
                                    id="ok",
                                    className="ms-auto",
                                    n_clicks=0
                                ),
                                dbc.Button(
                                    "Cancel",
                                    id="cancel",
                                    className="ms-auto",
                                    n_clicks=0
                                )
                            ]
                        )
                    ]
                ),
            ],
            id="modal",
            is_open=False,
            centered=True
        ),
    ]
)

app = dash.Dash(
    __name__,
    external_stylesheets=[dbc.themes.SLATE],
    meta_tags=[
        {'name': 'viewport',
         'content': 'width=device-width, initial-scale=1.0'
         }
    ]
)
app.layout = html.Div(
    [
        dbc.Button(
            "add data",
            id="open",
            n_clicks=0
        ),
        dcc.Input(id='added_data', type='text'),
        # ^^ simulate the new data via input
        dcc.Store(id='stored_data', data='initial value'),
        html.Div(id='message'),
        html.Div(id='store_content'),
        modal
    ]
)


@app.callback(
    [
        Output("modal", "is_open"),
        Output("message", "children"),
        Output("ok", "disabled"),
        Output('stored_data', 'data')
    ],
    [
        Input("open", "n_clicks"),
        Input("ok", "n_clicks"),
        Input("cancel", "n_clicks"),
    ],
    [
        State("modal", "is_open"),
        State("added_data", "value"),
        State("ok", "disabled"),
        State('stored_data', 'data')
    ],
    prevent_initial_call=True
)
def toggle_modal(open_modal, ok, cancel, is_open, added_data, status_ok_btn, current_store_data):
    # which button triggered the callback?
    trigger = ctx.triggered_id

    # new data has been added
    if trigger == 'open':
        # check data.
        if added_data != 'correct data':
            # if not correct, set disabled=True for the OK button
            return not is_open, 'just opened', True, current_store_data
        else:
            # if correct, set disabled=False (button is clickable) for the OK button
            return not is_open, 'just opened', False, current_store_data

    # ok button has been clicked
    if trigger == 'ok':
        # ok has been clicked, update the dcc.Store() with the added data
        return not is_open, 'you just confirmed', status_ok_btn, added_data

    # cancel button has been clicked
    if trigger == 'cancel':
        # cancel has been clicked, do nothing
        return not is_open, 'you just canceled', status_ok_btn, current_store_data


@app.callback(
    Output('store_content', 'children'),
    Input('stored_data', 'data'),
)
def show_data(data):
    return data


if __name__ == '__main__':
    app.run(debug=True, port=8051)

mred modal