I have a complex, dynamic list like structure with children that contain buttons. When patching a single child of that list, this causes a callback with a triggered_id of an unrelated button in another child that was not patched.
The following is a MRE with just three buttons where the click on button will update the displayed click count in the button’s name using a patch. See that confirming the update for any button immediately triggers the display_confirm callback and reopens the confirmation dialog for the first button “Foo”.
Is this really intended behaviour or should patch behave differently?
from dash import ALL, Dash, Input, Output, Patch, State, callback, ctx, dcc, html
from dash.exceptions import PreventUpdate
app = Dash()
button_names = ["Foo", "Bar", "Baz"]
app.layout = html.Div([
dcc.ConfirmDialog(
id="confirm-dialog",
message="Update click count for button?",
),
dcc.Store(
id="last-button-clicked-id",
data=None
),
html.Div(id="button-container", children=[
html.Button(f"{button_names[i]} 0", id={"type": "button", "index": i}) for i in range(len(button_names))
]),
])
@callback(
Output("confirm-dialog", "displayed"),
Output("confirm-dialog", "message"),
Output("last-button-clicked-id", "data"),
Input({"type": "button", "index": ALL}, "n_clicks"),
)
def display_confirm(n_clicks):
""" Display the confirm dialog when a button is clicked and store the id of the clicked button."""
if ctx.triggered_id:
idx = ctx.triggered_id["index"]
return True, f"Update click count for button {button_names[idx]}?", ctx.triggered_id
else:
raise PreventUpdate()
@callback(
Output("button-container", "children"),
Input("confirm-dialog", "submit_n_clicks"),
State("last-button-clicked-id", "data"),
State({"type": "button", "index": ALL}, "n_clicks")
)
def patch_button_click_count(submit_n_clicks, last_button_clicked_id, button_n_clicks):
"""Update the number of clicks on confirm by patching the relevant child of the button container."""
if submit_n_clicks:
idx = last_button_clicked_id["index"]
patch = Patch()
# update the button's name (however, even without the following line and an empty patch the strange behaviour persists)
patch[idx]["props"]["children"] = f"{button_names[idx]} {button_n_clicks[idx]}"
return patch
else:
raise PreventUpdate()
if __name__ == '__main__':
app.run(debug=True)
I’m not looking for a solution, that works around the issue by changing the button’s name directly instead of patching the containing div, because this is easy in this example, but would mean a lot effort in my real code. Right now I workaround the issue by storing the n_clicks and raising PreventUpdate in display_confirm when stored n_clicks equals the triggering n_clicks.
Something similar was asked before by @mingw64, but with no conclusive answer and not really focussing on the issue that a component that was not touched at all triggers a callback.