Skip to content

Commit 700b706

Browse files
authored
Merge pull request #3268 from BSd3v/tabs-rework
allows for parents to listen to descendent updates
2 parents 76a8e16 + 4ae8778 commit 700b706

File tree

5 files changed

+154
-10
lines changed

5 files changed

+154
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
88
- [#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
99
- [#3265](https://github.com/plotly/dash/pull/3265) Fixed issue where the resize of graphs was cancelling others
1010

11+
## Added
12+
- [#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`
1113

1214
## [3.0.2] - 2025-04-01
1315

components/dash-core-components/src/components/Tabs.react.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,6 @@ const EnhancedTab = ({
121121
);
122122
};
123123

124-
EnhancedTab.defaultProps = {
125-
loading_state: {
126-
is_loading: false,
127-
component_name: '',
128-
prop_name: '',
129-
},
130-
};
131-
132124
/**
133125
* A Dash component that lets you render pages with tabs - the Tabs component's children
134126
* can be dcc.Tab components, which can hold a label that will be displayed as a tab, and can in turn hold
@@ -439,3 +431,5 @@ Tabs.propTypes = {
439431
*/
440432
persistence_type: PropTypes.oneOf(['local', 'session', 'memory']),
441433
};
434+
435+
Tabs.dashChildrenUpdate = true;

dash/dash-renderer/src/wrapper/selectors.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,85 @@
11
import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component';
2-
import {getComponentLayout, stringifyPath} from './wrapping';
2+
import {
3+
getComponentLayout,
4+
stringifyPath,
5+
checkDashChildrenUpdate
6+
} from './wrapping';
7+
import {pathOr} from 'ramda';
38

49
type SelectDashProps = [DashComponent, BaseDashProps, number, object, string];
510

11+
interface ChangedPropsRecord {
12+
hash: number;
13+
changedProps: Record<string, any>;
14+
renderType: string;
15+
}
16+
17+
interface Hashes {
18+
[key: string]: any; // Index signature for string keys with number values
19+
}
20+
21+
const previousHashes: Hashes = {};
22+
23+
const isFirstLevelPropsChild = (
24+
updatedPath: string,
25+
strPath: string
26+
): [boolean, string[]] => {
27+
const updatedSegments = updatedPath.split(',');
28+
const fullSegments = strPath.split(',');
29+
30+
// Check that strPath actually starts with updatedPath
31+
const startsWithPath = fullSegments.every(
32+
(seg, i) => updatedSegments[i] === seg
33+
);
34+
35+
if (!startsWithPath) return [false, []];
36+
37+
// Get the remaining path after the prefix
38+
const remainingSegments = updatedSegments.slice(fullSegments.length);
39+
40+
const propsCount = remainingSegments.filter(s => s === 'props').length;
41+
42+
return [propsCount < 2, remainingSegments];
43+
};
44+
45+
function determineChangedProps(
46+
state: any,
47+
strPath: string
48+
): ChangedPropsRecord {
49+
let combinedHash = 0;
50+
let renderType: any; // Default render type, adjust as needed
51+
const changedProps: Record<string, any> = {};
52+
Object.entries(state.layoutHashes).forEach(([updatedPath, pathHash]) => {
53+
const [descendant, remainingSegments] = isFirstLevelPropsChild(
54+
updatedPath,
55+
strPath
56+
);
57+
if (descendant) {
58+
const previousHash: any = pathOr({}, [updatedPath], previousHashes);
59+
combinedHash += pathOr(0, ['hash'], pathHash);
60+
if (previousHash !== pathHash) {
61+
if (updatedPath !== strPath) {
62+
Object.assign(changedProps, {[remainingSegments[1]]: true});
63+
renderType = 'components';
64+
} else {
65+
Object.assign(
66+
changedProps,
67+
pathOr({}, ['changedProps'], pathHash)
68+
);
69+
renderType = pathOr({}, ['renderType'], pathHash);
70+
}
71+
previousHashes[updatedPath] = pathHash;
72+
}
73+
}
74+
});
75+
76+
return {
77+
hash: combinedHash,
78+
changedProps,
79+
renderType
80+
};
81+
}
82+
683
export const selectDashProps =
784
(componentPath: DashLayoutPath) =>
885
(state: any): SelectDashProps => {
@@ -12,7 +89,12 @@ export const selectDashProps =
1289
// Then it can be easily compared without having to compare the props.
1390
const strPath = stringifyPath(componentPath);
1491

15-
const hash = state.layoutHashes[strPath];
92+
let hash;
93+
if (checkDashChildrenUpdate(c)) {
94+
hash = determineChangedProps(state, strPath);
95+
} else {
96+
hash = state.layoutHashes[strPath];
97+
}
1698
let h = 0;
1799
let changedProps: object = {};
18100
let renderType = '';

dash/dash-renderer/src/wrapper/wrapping.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,14 @@ export function checkRenderTypeProp(componentDefinition: any) {
7272
)
7373
);
7474
}
75+
76+
export function checkDashChildrenUpdate(componentDefinition: any) {
77+
return (
78+
'dashChildrenUpdate' in
79+
pathOr(
80+
{},
81+
[componentDefinition?.namespace, componentDefinition?.type],
82+
window as any
83+
)
84+
);
85+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from dash import dcc, html, Input, Output, Patch, Dash
2+
3+
4+
def test_dcl001_descendant_tabs(dash_duo):
5+
app = Dash()
6+
7+
app.layout = html.Div(
8+
[
9+
html.Button("Enable Tabs", id="button", n_clicks=0),
10+
html.Button("Add Tabs", id="add_button", n_clicks=0),
11+
dcc.Store(id="store-data", data=None),
12+
dcc.Tabs(
13+
[
14+
dcc.Tab(label="Tab A", value="tab-a", id="tab-a", disabled=True),
15+
dcc.Tab(label="Tab B", value="tab-b", id="tab-b", disabled=True),
16+
],
17+
id="tabs",
18+
value="tab-a",
19+
),
20+
]
21+
)
22+
23+
@app.callback(Output("store-data", "data"), Input("button", "n_clicks"))
24+
def update_store_data(clicks):
25+
if clicks > 0:
26+
return {"data": "available"}
27+
return None
28+
29+
@app.callback(
30+
Output("tabs", "children"),
31+
Input("add_button", "n_clicks"),
32+
prevent_initial_call=True,
33+
)
34+
def add_tabs(n):
35+
children = Patch()
36+
children.append(dcc.Tab(label=f"{n}", value=f"{n}", id=f"test-{n}"))
37+
return children
38+
39+
@app.callback(
40+
Output("tab-a", "disabled"),
41+
Output("tab-b", "disabled"),
42+
Input("store-data", "data"),
43+
)
44+
def toggle_tabs(store_data):
45+
if store_data is not None and "data" in store_data:
46+
return False, False
47+
return True, True
48+
49+
dash_duo.start_server(app)
50+
dash_duo.wait_for_text_to_equal("#button", "Enable Tabs")
51+
dash_duo.find_element("#tab-a.tab--disabled")
52+
dash_duo.find_element("#button").click()
53+
dash_duo.find_element("#tab-a:not(.tab--disabled)")
54+
dash_duo.find_element("#add_button").click()
55+
dash_duo.find_element("#test-1:not(.tab--disabled)")

0 commit comments

Comments
 (0)