Skip to content

Commit 5b2b768

Browse files
authored
Merge branch 'dev' into load-inside-callback
2 parents 0010ed1 + f7f8fb4 commit 5b2b768

File tree

9 files changed

+162
-34
lines changed

9 files changed

+162
-34
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55
## [UNRELEASED]
66

77
## Added
8+
9+
- [#2795](https://github.com/plotly/dash/pull/2795) Allow list of components to be passed as layout.
810
- [2760](https://github.com/plotly/dash/pull/2760) New additions to dcc.Loading resolving multiple issues:
911
- `delay_show` and `delay_hide` props to prevent flickering during brief loading periods (similar to Dash Bootstrap Components dbc.Spinner)
1012
- `overlay_style` for styling the loading overlay, such as setting visibility and opacity for children

components/dash-core-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,6 @@
103103
"react-dom": ">=16"
104104
},
105105
"browserslist": [
106-
"last 8 years and not dead"
106+
"last 9 years and not dead"
107107
]
108108
}

components/dash-html-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@
5959
"/dash_html_components/*{.js,.map}"
6060
],
6161
"browserslist": [
62-
"last 8 years and not dead"
62+
"last 9 years and not dead"
6363
]
6464
}

components/dash-table/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,6 @@
125125
"npm": ">=6.1.0"
126126
},
127127
"browserslist": [
128-
"last 8 years and not dead"
128+
"last 9 years and not dead"
129129
]
130130
}

dash/_validate.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -398,12 +398,13 @@ def validate_index(name, checks, index):
398398

399399

400400
def validate_layout_type(value):
401-
if not isinstance(value, (Component, patch_collections_abc("Callable"))):
401+
if not isinstance(
402+
value, (Component, patch_collections_abc("Callable"), list, tuple)
403+
):
402404
raise exceptions.NoLayoutException(
403405
"""
404-
Layout must be a single dash component
406+
Layout must be a single dash component, a list of dash components,
405407
or a function that returns a dash component.
406-
Cannot be a tuple (are there any trailing commas?)
407408
"""
408409
)
409410

@@ -418,18 +419,34 @@ def validate_layout(layout, layout_value):
418419
"""
419420
)
420421

421-
layout_id = stringify_id(getattr(layout_value, "id", None))
422+
component_ids = set()
422423

423-
component_ids = {layout_id} if layout_id else set()
424-
for component in layout_value._traverse(): # pylint: disable=protected-access
425-
component_id = stringify_id(getattr(component, "id", None))
426-
if component_id and component_id in component_ids:
427-
raise exceptions.DuplicateIdError(
428-
f"""
429-
Duplicate component id found in the initial layout: `{component_id}`
430-
"""
431-
)
432-
component_ids.add(component_id)
424+
def _validate(value):
425+
def _validate_id(comp):
426+
component_id = stringify_id(getattr(comp, "id", None))
427+
if component_id and component_id in component_ids:
428+
raise exceptions.DuplicateIdError(
429+
f"""
430+
Duplicate component id found in the initial layout: `{component_id}`
431+
"""
432+
)
433+
component_ids.add(component_id)
434+
435+
_validate_id(value)
436+
437+
for component in value._traverse(): # pylint: disable=protected-access
438+
_validate_id(component)
439+
440+
if isinstance(layout_value, (list, tuple)):
441+
for component in layout_value:
442+
if isinstance(component, (Component,)):
443+
_validate(component)
444+
else:
445+
raise exceptions.NoLayoutException(
446+
"List of components as layout must be a list of components only."
447+
)
448+
else:
449+
_validate(layout_value)
433450

434451

435452
def validate_template(template):

dash/dash-renderer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,6 @@
8888
],
8989
"prettier": "@plotly/prettier-config-dash",
9090
"browserslist": [
91-
"last 8 years and not dead"
91+
"last 9 years and not dead"
9292
]
9393
}

dash/dash-renderer/src/APIController.react.js

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {getAppState} from './reducers/constants';
2121
import {STATUS} from './constants/constants';
2222
import {getLoadingState, getLoadingHash} from './utils/TreeContainer';
2323
import wait from './utils/wait';
24+
import isSimpleComponent from './isSimpleComponent';
2425

2526
export const DashContext = createContext({});
2627

@@ -97,20 +98,44 @@ const UnconnectedContainer = props => {
9798

9899
content = (
99100
<DashContext.Provider value={provider.current}>
100-
<TreeContainer
101-
_dashprivate_error={error}
102-
_dashprivate_layout={layout}
103-
_dashprivate_loadingState={getLoadingState(
104-
layout,
105-
[],
106-
loadingMap
107-
)}
108-
_dashprivate_loadingStateHash={getLoadingHash(
109-
[],
110-
loadingMap
111-
)}
112-
_dashprivate_path={JSON.stringify([])}
113-
/>
101+
{Array.isArray(layout) ? (
102+
layout.map((c, i) =>
103+
isSimpleComponent(c) ? (
104+
c
105+
) : (
106+
<TreeContainer
107+
_dashprivate_error={error}
108+
_dashprivate_layout={c}
109+
_dashprivate_loadingState={getLoadingState(
110+
c,
111+
[i],
112+
loadingMap
113+
)}
114+
_dashprivate_loadingStateHash={getLoadingHash(
115+
[i],
116+
loadingMap
117+
)}
118+
_dashprivate_path={`[${i}]`}
119+
key={i}
120+
/>
121+
)
122+
)
123+
) : (
124+
<TreeContainer
125+
_dashprivate_error={error}
126+
_dashprivate_layout={layout}
127+
_dashprivate_loadingState={getLoadingState(
128+
layout,
129+
[],
130+
loadingMap
131+
)}
132+
_dashprivate_loadingStateHash={getLoadingHash(
133+
[],
134+
loadingMap
135+
)}
136+
_dashprivate_path={'[]'}
137+
/>
138+
)}
114139
</DashContext.Provider>
115140
);
116141
} else {
@@ -216,7 +241,7 @@ UnconnectedContainer.propTypes = {
216241
graphs: PropTypes.object,
217242
hooks: PropTypes.object,
218243
layoutRequest: PropTypes.object,
219-
layout: PropTypes.object,
244+
layout: PropTypes.any,
220245
loadingMap: PropTypes.any,
221246
history: PropTypes.any,
222247
error: PropTypes.object,

requires-ci.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ black==22.3.0
33
dash-flow-example==0.0.5
44
dash-dangerously-set-inner-html
55
flake8==7.0.0
6-
flaky==3.7.0
6+
flaky==3.8.1
77
flask-talisman==1.0.0
88
mimesis<=11.1.0
99
mock==4.0.3

tests/integration/test_integration.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,87 @@ def test_inin027_multi_page_without_pages_folder(dash_duo):
427427
del dash.page_registry["not_found_404"]
428428

429429
assert not dash_duo.get_logs()
430+
431+
432+
def test_inin028_layout_as_list(dash_duo):
433+
app = Dash()
434+
435+
app.layout = [
436+
html.Div("one", id="one"),
437+
html.Div("two", id="two"),
438+
html.Button("direct", id="direct"),
439+
html.Div(id="direct-output"),
440+
html.Div([html.Button("nested", id="nested"), html.Div(id="nested-output")]),
441+
]
442+
443+
@app.callback(
444+
Output("direct-output", "children"),
445+
Input("direct", "n_clicks"),
446+
prevent_initial_call=True,
447+
)
448+
def on_direct_click(n_clicks):
449+
return f"Clicked {n_clicks} times"
450+
451+
@app.callback(
452+
Output("nested-output", "children"),
453+
Input("nested", "n_clicks"),
454+
prevent_initial_call=True,
455+
)
456+
def on_nested_click(n_clicks):
457+
return f"Clicked {n_clicks} times"
458+
459+
dash_duo.start_server(app)
460+
461+
dash_duo.wait_for_text_to_equal("#one", "one")
462+
dash_duo.wait_for_text_to_equal("#two", "two")
463+
464+
dash_duo.wait_for_element("#direct").click()
465+
dash_duo.wait_for_text_to_equal("#direct-output", "Clicked 1 times")
466+
467+
dash_duo.wait_for_element("#nested").click()
468+
dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times")
469+
470+
471+
def test_inin029_layout_as_list_with_pages(dash_duo):
472+
app = Dash(use_pages=True, pages_folder="")
473+
474+
dash.register_page(
475+
"list-pages",
476+
"/",
477+
layout=[
478+
html.Div("one", id="one"),
479+
html.Div("two", id="two"),
480+
html.Button("direct", id="direct"),
481+
html.Div(id="direct-output"),
482+
html.Div(
483+
[html.Button("nested", id="nested"), html.Div(id="nested-output")]
484+
),
485+
],
486+
)
487+
488+
@app.callback(
489+
Output("direct-output", "children"),
490+
Input("direct", "n_clicks"),
491+
prevent_initial_call=True,
492+
)
493+
def on_direct_click(n_clicks):
494+
return f"Clicked {n_clicks} times"
495+
496+
@app.callback(
497+
Output("nested-output", "children"),
498+
Input("nested", "n_clicks"),
499+
prevent_initial_call=True,
500+
)
501+
def on_nested_click(n_clicks):
502+
return f"Clicked {n_clicks} times"
503+
504+
dash_duo.start_server(app)
505+
506+
dash_duo.wait_for_text_to_equal("#one", "one")
507+
dash_duo.wait_for_text_to_equal("#two", "two")
508+
509+
dash_duo.wait_for_element("#direct").click()
510+
dash_duo.wait_for_text_to_equal("#direct-output", "Clicked 1 times")
511+
512+
dash_duo.wait_for_element("#nested").click()
513+
dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times")

0 commit comments

Comments
 (0)