From 3a229ebf021796b2f5a09c6ef640c7a3878466e8 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Fri, 27 Jun 2025 12:54:23 -0700 Subject: [PATCH 1/5] Allow dict id in target_components --- .../src/components/Loading.react.js | 14 ++++- .../loading/test_loading_component.py | 55 +++++++++++-------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index f703056c8b..167a6a6e0d 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -8,6 +8,17 @@ import CubeSpinner from '../fragments/Loading/spinners/CubeSpinner.jsx'; import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx'; import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx'; +const stringifyId = id => { + if (typeof id !== 'object') { + return id; + } + const stringifyVal = v => (v && v.wild) || JSON.stringify(v); + const parts = Object.keys(id) + .sort() + .map(k => JSON.stringify(k) + ':' + stringifyVal(id[k])); + return '{' + parts.join(',') + '}'; +}; + const spinnerComponentOptions = { graph: GraphSpinner, cube: CubeSpinner, @@ -40,7 +51,7 @@ const loadingSelector = (componentPath, targetComponents) => state => { if ( targetComponents && !any(l => { - const target = targetComponents[l.id]; + const target = targetComponents[stringifyId(l.id)]; if (!target) { return false; } @@ -84,6 +95,7 @@ function Loading({ custom_spinner, }) { const ctx = window.dash_component_api.useDashContext(); + const loading = ctx.useSelector( loadingSelector(ctx.componentPath, target_components), equals diff --git a/components/dash-core-components/tests/integration/loading/test_loading_component.py b/components/dash-core-components/tests/integration/loading/test_loading_component.py index a7500daeba..eab6f97284 100644 --- a/components/dash-core-components/tests/integration/loading/test_loading_component.py +++ b/components/dash-core-components/tests/integration/loading/test_loading_component.py @@ -1,5 +1,6 @@ from multiprocessing import Lock from dash import Dash, Input, Output, dcc, html +from dash.dependencies import stringify_id from dash.testing import wait import time @@ -414,9 +415,9 @@ def updateDiv(n_clicks): assert dash_dcc.get_logs() == [] -# multiple components, only one triggers the spinner -def test_ldcp010_loading_component_target_components(dash_dcc): - +# update multiple props of same component, only targeted id/prop triggers spinner +# test that target_components id can be a dict id +def test_ldcp011_loading_component_target_components(dash_dcc): lock = Lock() app = Dash(__name__) @@ -425,53 +426,61 @@ def test_ldcp010_loading_component_target_components(dash_dcc): [ dcc.Loading( [ - html.Button(id="btn-1"), + html.Button(id={"type": "button", "index": "one"}), html.Button(id="btn-2"), + html.Button(id="btn-3"), ], className="loading-1", - target_components={"btn-2": "children"}, + target_components={ + stringify_id({"type": "button", "index": "one"}): "className" + }, ) ], id="root", ) - @app.callback(Output("btn-1", "children"), [Input("btn-2", "n_clicks")]) + @app.callback( + Output({"type": "button", "index": "one"}, "children"), + [Input("btn-2", "n_clicks")], + ) def updateDiv1(n_clicks): if n_clicks: with lock: return "changed 1" - return "content 1" - @app.callback(Output("btn-2", "children"), [Input("btn-1", "n_clicks")]) + @app.callback( + Output({"type": "button", "index": "one"}, "className"), + [Input("btn-3", "n_clicks")], + ) def updateDiv2(n_clicks): if n_clicks: with lock: - return "changed 2" - - return "content 2" + return "new-class" + return "" dash_dcc.start_server(app) - dash_dcc.wait_for_text_to_equal("#btn-1", "content 1") - dash_dcc.wait_for_text_to_equal("#btn-2", "content 2") + btn1id = "#" + stringify_id({"type": "button", "index": "one"}) - with lock: - dash_dcc.find_element("#btn-1").click() - - dash_dcc.find_element(".loading-1 .dash-spinner") - dash_dcc.wait_for_text_to_equal("#btn-2", "") - - dash_dcc.wait_for_text_to_equal("#btn-2", "changed 2") + dash_dcc.wait_for_text_to_equal(btn1id, "content 1") with lock: dash_dcc.find_element("#btn-2").click() - spinners = dash_dcc.find_elements(".loading-1 .dash-spinner") - dash_dcc.wait_for_text_to_equal("#btn-1", "") - dash_dcc.wait_for_text_to_equal("#btn-1", "changed 1") + spinners = dash_dcc.find_elements(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal(btn1id, "") + dash_dcc.wait_for_text_to_equal(btn1id, "changed 1") assert spinners == [] + with lock: + dash_dcc.find_element("#btn-3").click() + + dash_dcc.find_element(".loading-1 .dash-spinner") + dash_dcc.wait_for_text_to_equal(btn1id, "") + + dash_dcc.wait_for_class_to_equal(btn1id, "new-class") + assert dash_dcc.get_logs() == [] From a7866b7f79df90b52dcc418d3f6dbe0931a4f6e2 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Mon, 30 Jun 2025 10:42:24 -0700 Subject: [PATCH 2/5] changed where id is stringified --- .../src/components/Loading.react.js | 13 +------------ dash/dash-renderer/src/actions/callbacks.ts | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/components/dash-core-components/src/components/Loading.react.js b/components/dash-core-components/src/components/Loading.react.js index 9acb60b6a7..60f5c2f188 100644 --- a/components/dash-core-components/src/components/Loading.react.js +++ b/components/dash-core-components/src/components/Loading.react.js @@ -8,17 +8,6 @@ import CubeSpinner from '../fragments/Loading/spinners/CubeSpinner.jsx'; import CircleSpinner from '../fragments/Loading/spinners/CircleSpinner.jsx'; import DotSpinner from '../fragments/Loading/spinners/DotSpinner.jsx'; -const stringifyId = id => { - if (typeof id !== 'object') { - return id; - } - const stringifyVal = v => (v && v.wild) || JSON.stringify(v); - const parts = Object.keys(id) - .sort() - .map(k => JSON.stringify(k) + ':' + stringifyVal(id[k])); - return '{' + parts.join(',') + '}'; -}; - const spinnerComponentOptions = { graph: GraphSpinner, cube: CubeSpinner, @@ -51,7 +40,7 @@ const loadingSelector = (componentPath, targetComponents) => state => { if ( targetComponents && !any(l => { - const target = targetComponents[stringifyId(l.id)]; + const target = targetComponents[l.id]; if (!target) { return false; } diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index eae5682cb4..73d4089b5b 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -753,7 +753,7 @@ export function executeCallback( const loadingOutputs = outputs.map(out => ({ path: getPath(paths, out.id), property: out.property?.split('@')[0], - id: out.id + id: (window as any).dash_component_api.stringifyId(out.id) })); dispatch(loading(loadingOutputs)); try { From b696ab95479326b412936f3132acfd39c9b7c1fe Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Mon, 30 Jun 2025 10:43:09 -0700 Subject: [PATCH 3/5] added stringify_id to init --- dash/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dash/__init__.py b/dash/__init__.py index 92d9e45a12..6909c3aa7e 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -30,6 +30,7 @@ CeleryManager, DiskcacheManager, ) +from ._utils import stringify_id # noqa: F401,E402 from ._pages import register_page, PAGE_REGISTRY as page_registry # noqa: F401,E402 @@ -90,4 +91,5 @@ def _jupyter_nbextension_paths(): "jupyter_dash", "ctx", "hooks", + "stringify_id", ] From b6b2770e0bfdc8d147c5778e89023bf123280333 Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Mon, 30 Jun 2025 10:49:56 -0700 Subject: [PATCH 4/5] add changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce01e05eea..16d4fee49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [UNRELEASED] + +## Fixed +- [#3353](https://github.com/plotly/dash/pull/3353) Support pattern-matching/dict ids in `dcc.Loading` `target_components` + + # [3.1.1] - 2025-06-29 ## Fixed From 91626f175a4f9a67c0e7033397c88c2dca78685e Mon Sep 17 00:00:00 2001 From: AnnMarueW Date: Mon, 30 Jun 2025 12:25:05 -0700 Subject: [PATCH 5/5] fixed stringifiyId --- dash/dash-renderer/src/actions/callbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 73d4089b5b..e77bf803aa 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -753,7 +753,7 @@ export function executeCallback( const loadingOutputs = outputs.map(out => ({ path: getPath(paths, out.id), property: out.property?.split('@')[0], - id: (window as any).dash_component_api.stringifyId(out.id) + id: stringifyId(out.id) })); dispatch(loading(loadingOutputs)); try {