diff --git a/CHANGELOG.md b/CHANGELOG.md index 2358e7a240..f987977a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [UNRELEASED] + +### Added +- [#2695](https://github.com/plotly/dash/pull/2695) Adds `triggered_id` to `dash_clientside.callback_context`. Fixes [#2692](https://github.com/plotly/dash/issues/2692) + ## [2.14.2] - 2023-11-27 ## Fixed diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index d936d6f915..3390869dee 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -262,6 +262,9 @@ async function handleClientside( prop_id: prop_id, value: inputDict[prop_id] })); + dc.callback_context.triggered_id = getTriggeredId( + payload.changedPropIds + ); dc.callback_context.inputs_list = inputs; dc.callback_context.inputs = inputDict; dc.callback_context.states_list = state; @@ -576,6 +579,18 @@ function inputsToDict(inputs_list: any) { return inputs; } +function getTriggeredId(triggered: string[]): string | object | undefined { + // for regular callbacks, takes the first triggered prop_id, e.g. "btn.n_clicks" and returns "btn" + // for pattern matching callback, e.g. '{"index":0, "type":"btn"}' and returns {index:0, type: "btn"}' + if (triggered && triggered.length) { + let componentId = triggered[0].split('.')[0]; + if (componentId.startsWith('{')) { + componentId = JSON.parse(componentId); + } + return componentId; + } +} + export function executeCallback( cb: IPrioritizedCallback, config: any, diff --git a/tests/integration/clientside/assets/clientside.js b/tests/integration/clientside/assets/clientside.js index 6f851d1f6b..c84e369da6 100644 --- a/tests/integration/clientside/assets/clientside.js +++ b/tests/integration/clientside/assets/clientside.js @@ -63,6 +63,12 @@ window.dash_clientside.clientside = { return triggered.map(t => `${t.prop_id} = ${t.value}`).join(', '); }, + triggered_id_to_str: function(n_clicks0, n_clicks1) { + const triggered = dash_clientside.callback_context.triggered_id; + const triggered_id = typeof triggered === "string" ? triggered : triggered.btn1 + return triggered_id + }, + inputs_to_str: function(n_clicks0, n_clicks1) { const inputs = dash_clientside.callback_context.inputs; const keys = Object.keys(inputs); diff --git a/tests/integration/clientside/test_clientside.py b/tests/integration/clientside/test_clientside.py index 150da399e4..405acf0268 100644 --- a/tests/integration/clientside/test_clientside.py +++ b/tests/integration/clientside/test_clientside.py @@ -829,3 +829,42 @@ def test_clsd019_clientside_inline_promise(dash_duo): dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#output-div", "initial-inline") + + +def test_clsd020_clientside_callback_context_triggered_id(dash_duo): + app = Dash(__name__, assets_folder="assets") + + app.layout = html.Div( + [ + html.Button("btn0", id="btn0"), + html.Button("btn1:0", id={"btn1": 0}), + html.Button("btn1:1", id={"btn1": 1}), + html.Button("btn1:2", id={"btn1": 2}), + html.Div(id="output-clientside", style={"font-family": "monospace"}), + ] + ) + + app.clientside_callback( + ClientsideFunction(namespace="clientside", function_name="triggered_id_to_str"), + Output("output-clientside", "children"), + [Input("btn0", "n_clicks"), Input({"btn1": ALL}, "n_clicks")], + ) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#output-clientside", "") + + dash_duo.find_element("#btn0").click() + + dash_duo.wait_for_text_to_equal( + "#output-clientside", + "btn0", + ) + + dash_duo.find_element("button[id*='btn1\":0']").click() + + dash_duo.wait_for_text_to_equal("#output-clientside", "0") + + dash_duo.find_element("button[id*='btn1\":2']").click() + + dash_duo.wait_for_text_to_equal("#output-clientside", "2")