From 57746d8ee9701d069ff726b34ca6fd9b02ea3af1 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:29:13 -0400 Subject: [PATCH 1/8] allows for parents to listen to descendent updates --- .../src/components/Tabs.react.js | 2 + dash/dash-renderer/src/wrapper/selectors.ts | 38 ++++++++++++++++++- dash/dash-renderer/src/wrapper/wrapping.ts | 11 ++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index a365925bdb..3475fcbd5c 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -439,3 +439,5 @@ Tabs.propTypes = { */ persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), }; + +Tabs.childrenLayoutHashes = true diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 46930bc47e..5483a313fc 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -1,8 +1,37 @@ import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component'; -import {getComponentLayout, stringifyPath} from './wrapping'; +import {getComponentLayout, stringifyPath, checkChildrenLayoutHashes} from './wrapping'; +import {pathOr} from 'ramda' type SelectDashProps = [DashComponent, BaseDashProps, number, object, string]; +interface ChangedPropsRecord { + hash: number; + changedProps: Record; + renderType: string; +} + +const previousHashes = {} + +function determineChangedProps(state: any, strPath: string): ChangedPropsRecord { + let combinedHash = 0; + let renderType = 'update'; // Default render type, adjust as needed + Object.entries(state.layoutHashes).forEach(([updatedPath, pathHash]) => { + if (updatedPath.startsWith(strPath)) { + const previousHash: any = pathOr({}, [updatedPath], previousHashes); + combinedHash += pathOr(0, ['hash'], pathHash) + if (previousHash !== pathHash) { + previousHash[updatedPath] = pathHash + } + } + }); + + return { + hash: combinedHash, + changedProps: {}, + renderType + }; +} + export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { @@ -12,7 +41,12 @@ export const selectDashProps = // Then it can be easily compared without having to compare the props. const strPath = stringifyPath(componentPath); - const hash = state.layoutHashes[strPath]; + let hash; + if (checkChildrenLayoutHashes(c)) { + hash = determineChangedProps(state, strPath) + } else { + hash = state.layoutHashes[strPath]; + } let h = 0; let changedProps: object = {}; let renderType = ''; diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index 37d2c461ea..e6d7641a57 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -72,3 +72,14 @@ export function checkRenderTypeProp(componentDefinition: any) { ) ); } + +export function checkChildrenLayoutHashes(componentDefinition: any) { + return ( + 'childrenLayoutHashes' in + pathOr( + {}, + [componentDefinition?.namespace, componentDefinition?.type], + window as any + ) + ); +} From 1df1e0176286a4c7ed34303d0ee157b51557be99 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:49:05 -0400 Subject: [PATCH 2/8] fixing for lint --- .../src/components/Tabs.react.js | 2 +- dash/dash-renderer/src/wrapper/selectors.ts | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 3475fcbd5c..085aa80978 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -440,4 +440,4 @@ Tabs.propTypes = { persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), }; -Tabs.childrenLayoutHashes = true +Tabs.childrenLayoutHashes = true; diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 5483a313fc..1dd59a2d07 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -1,6 +1,10 @@ import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component'; -import {getComponentLayout, stringifyPath, checkChildrenLayoutHashes} from './wrapping'; -import {pathOr} from 'ramda' +import { + getComponentLayout, + stringifyPath, + checkChildrenLayoutHashes +} from './wrapping'; +import {pathOr} from 'ramda'; type SelectDashProps = [DashComponent, BaseDashProps, number, object, string]; @@ -10,17 +14,20 @@ interface ChangedPropsRecord { renderType: string; } -const previousHashes = {} +const previousHashes = {}; -function determineChangedProps(state: any, strPath: string): ChangedPropsRecord { +function determineChangedProps( + state: any, + strPath: string +): ChangedPropsRecord { let combinedHash = 0; - let renderType = 'update'; // Default render type, adjust as needed + const renderType = 'update'; // Default render type, adjust as needed Object.entries(state.layoutHashes).forEach(([updatedPath, pathHash]) => { if (updatedPath.startsWith(strPath)) { const previousHash: any = pathOr({}, [updatedPath], previousHashes); - combinedHash += pathOr(0, ['hash'], pathHash) + combinedHash += pathOr(0, ['hash'], pathHash); if (previousHash !== pathHash) { - previousHash[updatedPath] = pathHash + previousHash[updatedPath] = pathHash; } } }); @@ -43,7 +50,7 @@ export const selectDashProps = let hash; if (checkChildrenLayoutHashes(c)) { - hash = determineChangedProps(state, strPath) + hash = determineChangedProps(state, strPath); } else { hash = state.layoutHashes[strPath]; } From c7ca77367025ea54e190d1fc09cc3d36f816d053 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:59:11 -0400 Subject: [PATCH 3/8] adding test for descendant listening --- .../renderer/test_descendant_listening.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/integration/renderer/test_descendant_listening.py diff --git a/tests/integration/renderer/test_descendant_listening.py b/tests/integration/renderer/test_descendant_listening.py new file mode 100644 index 0000000000..b06ae95e3d --- /dev/null +++ b/tests/integration/renderer/test_descendant_listening.py @@ -0,0 +1,57 @@ +from dash import dcc, html, Input, Output, Patch, Dash + +from dash.testing.wait import until + + +def test_dcl001_descendant_tabs(dash_duo): + app = Dash() + + app.layout = html.Div( + [ + html.Button("Enable Tabs", id="button", n_clicks=0), + html.Button("Add Tabs", id="add_button", n_clicks=0), + dcc.Store(id="store-data", data=None), + dcc.Tabs( + [ + dcc.Tab(label="Tab A", value="tab-a", id="tab-a", disabled=True), + dcc.Tab(label="Tab B", value="tab-b", id="tab-b", disabled=True), + ], + id="tabs", + value="tab-a", + ), + ] + ) + + @app.callback(Output("store-data", "data"), Input("button", "n_clicks")) + def update_store_data(clicks): + if clicks > 0: + return {"data": "available"} + return None + + @app.callback( + Output("tabs", "children"), + Input("add_button", "n_clicks"), + prevent_initial_call=True, + ) + def add_tabs(n): + children = Patch() + children.append(dcc.Tab(label=f"{n}", value=f"{n}", id=f"test-{n}")) + return children + + @app.callback( + Output("tab-a", "disabled"), + Output("tab-b", "disabled"), + Input("store-data", "data"), + ) + def toggle_tabs(store_data): + if store_data is not None and "data" in store_data: + return False, False + return True, True + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#button", f"Enable Tabs") + dash_duo.find_element("#tab-a.tab--disabled") + dash_duo.find_element("#button").click() + dash_duo.find_element("#tab-a:not(.tab--disabled)") + dash_duo.find_element("#add_button").click() + dash_duo.find_element("#test-1:not(.tab--disabled)") From 8d94cc2c779b6b6f16bb5a89208779cd1b5344a5 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:01:28 -0400 Subject: [PATCH 4/8] adding changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b41ad382..ffae3c49df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3264](https://github.com/plotly/dash/pull/3264) Fixed an issue where moving components inside of children would not update the `setProps` path, leading to hashes being incorrect - [#3265](https://github.com/plotly/dash/pull/3265) Fixed issue where the resize of graphs was cancelling others +## Added +- [#3268](https://github.com/plotly/dash/pull/3268) Added the ability for component devs to subscribe to descendent updates by setting `childrenLayoutHashes = true` on the component, eg: `Tabs.childrenLayoutHashes = true` ## [3.0.2] - 2025-04-01 From a96ab642e5193976156f2b9880a426e921b4e838 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:01:03 -0400 Subject: [PATCH 5/8] updating to pull in the changed props and only goes down one descendant --- dash/dash-renderer/src/wrapper/selectors.ts | 51 +++++++++++++++++-- .../renderer/test_descendant_listening.py | 4 +- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 1dd59a2d07..b4d3f28e5b 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -14,27 +14,68 @@ interface ChangedPropsRecord { renderType: string; } -const previousHashes = {}; +interface Hashes { + [key: string]: any; // Index signature for string keys with number values +} + +const previousHashes: Hashes = {}; + +const isFirstLevelPropsChild = ( + updatedPath: string, + strPath: string +): [boolean, string[]] => { + const updatedSegments = updatedPath.split(','); + const fullSegments = strPath.split(','); + + // Check that strPath actually starts with updatedPath + const startsWithPath = fullSegments.every( + (seg, i) => updatedSegments[i] === seg + ); + + if (!startsWithPath) return [false, []]; + + // Get the remaining path after the prefix + const remainingSegments = updatedSegments.slice(fullSegments.length); + + const propsCount = remainingSegments.filter(s => s === 'props').length; + + return [propsCount < 2, remainingSegments]; +}; function determineChangedProps( state: any, strPath: string ): ChangedPropsRecord { let combinedHash = 0; - const renderType = 'update'; // Default render type, adjust as needed + let renderType: any; // Default render type, adjust as needed + const changedProps: Record = {}; Object.entries(state.layoutHashes).forEach(([updatedPath, pathHash]) => { - if (updatedPath.startsWith(strPath)) { + const [descendant, remainingSegments] = isFirstLevelPropsChild( + updatedPath, + strPath + ); + if (descendant) { const previousHash: any = pathOr({}, [updatedPath], previousHashes); combinedHash += pathOr(0, ['hash'], pathHash); if (previousHash !== pathHash) { - previousHash[updatedPath] = pathHash; + if (updatedPath !== strPath) { + Object.assign(changedProps, {[remainingSegments[1]]: true}); + renderType = 'components'; + } else { + Object.assign( + changedProps, + pathOr({}, ['changedProps'], pathHash) + ); + renderType = pathOr({}, ['renderType'], pathHash); + } + previousHashes[updatedPath] = pathHash; } } }); return { hash: combinedHash, - changedProps: {}, + changedProps, renderType }; } diff --git a/tests/integration/renderer/test_descendant_listening.py b/tests/integration/renderer/test_descendant_listening.py index b06ae95e3d..8bede0866e 100644 --- a/tests/integration/renderer/test_descendant_listening.py +++ b/tests/integration/renderer/test_descendant_listening.py @@ -1,7 +1,5 @@ from dash import dcc, html, Input, Output, Patch, Dash -from dash.testing.wait import until - def test_dcl001_descendant_tabs(dash_duo): app = Dash() @@ -49,7 +47,7 @@ def toggle_tabs(store_data): return True, True dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#button", f"Enable Tabs") + dash_duo.wait_for_text_to_equal("#button", "Enable Tabs") dash_duo.find_element("#tab-a.tab--disabled") dash_duo.find_element("#button").click() dash_duo.find_element("#tab-a:not(.tab--disabled)") From 4cbd6ffad2b6106dffee6fd915ac89f938f21029 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:31:48 -0400 Subject: [PATCH 6/8] updating attribute name from `childrenLayoutHashes` to `dashChildrenUpdate` --- CHANGELOG.md | 2 +- components/dash-core-components/src/components/Tabs.react.js | 2 +- dash/dash-renderer/src/wrapper/selectors.ts | 4 ++-- dash/dash-renderer/src/wrapper/wrapping.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffae3c49df..020cb95459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3265](https://github.com/plotly/dash/pull/3265) Fixed issue where the resize of graphs was cancelling others ## Added -- [#3268](https://github.com/plotly/dash/pull/3268) Added the ability for component devs to subscribe to descendent updates by setting `childrenLayoutHashes = true` on the component, eg: `Tabs.childrenLayoutHashes = true` +- [#3268](https://github.com/plotly/dash/pull/3268) Added the ability for component devs to subscribe to descendent updates by setting `dashChildrenUpdate = true` on the component, eg: `Tabs.dashChildrenUpdate = true` ## [3.0.2] - 2025-04-01 diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 085aa80978..585dacbaad 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -440,4 +440,4 @@ Tabs.propTypes = { persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), }; -Tabs.childrenLayoutHashes = true; +Tabs.dashChildrenUpdate = true; diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index b4d3f28e5b..b07713ed79 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -2,7 +2,7 @@ import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component'; import { getComponentLayout, stringifyPath, - checkChildrenLayoutHashes + checkDashChildrenUpdate } from './wrapping'; import {pathOr} from 'ramda'; @@ -90,7 +90,7 @@ export const selectDashProps = const strPath = stringifyPath(componentPath); let hash; - if (checkChildrenLayoutHashes(c)) { + if (checkDashChildrenUpdate(c)) { hash = determineChangedProps(state, strPath); } else { hash = state.layoutHashes[strPath]; diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index e6d7641a57..6a443990b6 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -73,9 +73,9 @@ export function checkRenderTypeProp(componentDefinition: any) { ); } -export function checkChildrenLayoutHashes(componentDefinition: any) { +export function checkDashChildrenUpdate(componentDefinition: any) { return ( - 'childrenLayoutHashes' in + 'dashChildrenUpdate' in pathOr( {}, [componentDefinition?.namespace, componentDefinition?.type], From 2e2aad5ca722ae89f9116b04ec891fe7005e91a2 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:33:08 -0400 Subject: [PATCH 7/8] deleting the defaultProps as they are unused --- .../dash-core-components/src/components/Tabs.react.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 585dacbaad..9d939879ac 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -121,13 +121,6 @@ const EnhancedTab = ({ ); }; -EnhancedTab.defaultProps = { - loading_state: { - is_loading: false, - component_name: '', - prop_name: '', - }, -}; /** * A Dash component that lets you render pages with tabs - the Tabs component's children From 4ae877820e014933f7e9e10b38d02cfcd6ead340 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:22:57 -0400 Subject: [PATCH 8/8] fixing for lint --- components/dash-core-components/src/components/Tabs.react.js | 1 - 1 file changed, 1 deletion(-) diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js index 9d939879ac..18acbe08c4 100644 --- a/components/dash-core-components/src/components/Tabs.react.js +++ b/components/dash-core-components/src/components/Tabs.react.js @@ -121,7 +121,6 @@ const EnhancedTab = ({ ); }; - /** * A Dash component that lets you render pages with tabs - the Tabs component's children * can be dcc.Tab components, which can hold a label that will be displayed as a tab, and can in turn hold