Skip to content

Commit

Permalink
Merge pull request #2795 from plotly/feat/list-as-layout
Browse files Browse the repository at this point in the history
Allow layout to be a list of components.
  • Loading branch information
T4rk1n authored Mar 21, 2024
2 parents 37965db + 41c8b09 commit f7f8fb4
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 33 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [UNRELEASED]

## Added

- [#2795](https://github.com/plotly/dash/pull/2795) Allow list of components to be passed as layout.
- [2760](https://github.com/plotly/dash/pull/2760) New additions to dcc.Loading resolving multiple issues:
- `delay_show` and `delay_hide` props to prevent flickering during brief loading periods (similar to Dash Bootstrap Components dbc.Spinner)
- `overlay_style` for styling the loading overlay, such as setting visibility and opacity for children
Expand Down
2 changes: 1 addition & 1 deletion components/dash-core-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,6 @@
"react-dom": ">=16"
},
"browserslist": [
"last 8 years and not dead"
"last 9 years and not dead"
]
}
2 changes: 1 addition & 1 deletion components/dash-html-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@
"/dash_html_components/*{.js,.map}"
],
"browserslist": [
"last 8 years and not dead"
"last 9 years and not dead"
]
}
2 changes: 1 addition & 1 deletion components/dash-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@
"npm": ">=6.1.0"
},
"browserslist": [
"last 8 years and not dead"
"last 9 years and not dead"
]
}
45 changes: 31 additions & 14 deletions dash/_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,13 @@ def validate_index(name, checks, index):


def validate_layout_type(value):
if not isinstance(value, (Component, patch_collections_abc("Callable"))):
if not isinstance(
value, (Component, patch_collections_abc("Callable"), list, tuple)
):
raise exceptions.NoLayoutException(
"""
Layout must be a single dash component
Layout must be a single dash component, a list of dash components,
or a function that returns a dash component.
Cannot be a tuple (are there any trailing commas?)
"""
)

Expand All @@ -418,18 +419,34 @@ def validate_layout(layout, layout_value):
"""
)

layout_id = stringify_id(getattr(layout_value, "id", None))
component_ids = set()

component_ids = {layout_id} if layout_id else set()
for component in layout_value._traverse(): # pylint: disable=protected-access
component_id = stringify_id(getattr(component, "id", None))
if component_id and component_id in component_ids:
raise exceptions.DuplicateIdError(
f"""
Duplicate component id found in the initial layout: `{component_id}`
"""
)
component_ids.add(component_id)
def _validate(value):
def _validate_id(comp):
component_id = stringify_id(getattr(comp, "id", None))
if component_id and component_id in component_ids:
raise exceptions.DuplicateIdError(
f"""
Duplicate component id found in the initial layout: `{component_id}`
"""
)
component_ids.add(component_id)

_validate_id(value)

for component in value._traverse(): # pylint: disable=protected-access
_validate_id(component)

if isinstance(layout_value, (list, tuple)):
for component in layout_value:
if isinstance(component, (Component,)):
_validate(component)
else:
raise exceptions.NoLayoutException(
"List of components as layout must be a list of components only."
)
else:
_validate(layout_value)


def validate_template(template):
Expand Down
2 changes: 1 addition & 1 deletion dash/dash-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@
],
"prettier": "@plotly/prettier-config-dash",
"browserslist": [
"last 8 years and not dead"
"last 9 years and not dead"
]
}
55 changes: 40 additions & 15 deletions dash/dash-renderer/src/APIController.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {getAppState} from './reducers/constants';
import {STATUS} from './constants/constants';
import {getLoadingState, getLoadingHash} from './utils/TreeContainer';
import wait from './utils/wait';
import isSimpleComponent from './isSimpleComponent';

export const DashContext = createContext({});

Expand Down Expand Up @@ -97,20 +98,44 @@ const UnconnectedContainer = props => {

content = (
<DashContext.Provider value={provider.current}>
<TreeContainer
_dashprivate_error={error}
_dashprivate_layout={layout}
_dashprivate_loadingState={getLoadingState(
layout,
[],
loadingMap
)}
_dashprivate_loadingStateHash={getLoadingHash(
[],
loadingMap
)}
_dashprivate_path={JSON.stringify([])}
/>
{Array.isArray(layout) ? (
layout.map((c, i) =>
isSimpleComponent(c) ? (
c
) : (
<TreeContainer
_dashprivate_error={error}
_dashprivate_layout={c}
_dashprivate_loadingState={getLoadingState(
c,
[i],
loadingMap
)}
_dashprivate_loadingStateHash={getLoadingHash(
[i],
loadingMap
)}
_dashprivate_path={`[${i}]`}
key={i}
/>
)
)
) : (
<TreeContainer
_dashprivate_error={error}
_dashprivate_layout={layout}
_dashprivate_loadingState={getLoadingState(
layout,
[],
loadingMap
)}
_dashprivate_loadingStateHash={getLoadingHash(
[],
loadingMap
)}
_dashprivate_path={'[]'}
/>
)}
</DashContext.Provider>
);
} else {
Expand Down Expand Up @@ -216,7 +241,7 @@ UnconnectedContainer.propTypes = {
graphs: PropTypes.object,
hooks: PropTypes.object,
layoutRequest: PropTypes.object,
layout: PropTypes.object,
layout: PropTypes.any,
loadingMap: PropTypes.any,
history: PropTypes.any,
error: PropTypes.object,
Expand Down
84 changes: 84 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,87 @@ def test_inin027_multi_page_without_pages_folder(dash_duo):
del dash.page_registry["not_found_404"]

assert not dash_duo.get_logs()


def test_inin028_layout_as_list(dash_duo):
app = Dash()

app.layout = [
html.Div("one", id="one"),
html.Div("two", id="two"),
html.Button("direct", id="direct"),
html.Div(id="direct-output"),
html.Div([html.Button("nested", id="nested"), html.Div(id="nested-output")]),
]

@app.callback(
Output("direct-output", "children"),
Input("direct", "n_clicks"),
prevent_initial_call=True,
)
def on_direct_click(n_clicks):
return f"Clicked {n_clicks} times"

@app.callback(
Output("nested-output", "children"),
Input("nested", "n_clicks"),
prevent_initial_call=True,
)
def on_nested_click(n_clicks):
return f"Clicked {n_clicks} times"

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#one", "one")
dash_duo.wait_for_text_to_equal("#two", "two")

dash_duo.wait_for_element("#direct").click()
dash_duo.wait_for_text_to_equal("#direct-output", "Clicked 1 times")

dash_duo.wait_for_element("#nested").click()
dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times")


def test_inin029_layout_as_list_with_pages(dash_duo):
app = Dash(use_pages=True, pages_folder="")

dash.register_page(
"list-pages",
"/",
layout=[
html.Div("one", id="one"),
html.Div("two", id="two"),
html.Button("direct", id="direct"),
html.Div(id="direct-output"),
html.Div(
[html.Button("nested", id="nested"), html.Div(id="nested-output")]
),
],
)

@app.callback(
Output("direct-output", "children"),
Input("direct", "n_clicks"),
prevent_initial_call=True,
)
def on_direct_click(n_clicks):
return f"Clicked {n_clicks} times"

@app.callback(
Output("nested-output", "children"),
Input("nested", "n_clicks"),
prevent_initial_call=True,
)
def on_nested_click(n_clicks):
return f"Clicked {n_clicks} times"

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#one", "one")
dash_duo.wait_for_text_to_equal("#two", "two")

dash_duo.wait_for_element("#direct").click()
dash_duo.wait_for_text_to_equal("#direct-output", "Clicked 1 times")

dash_duo.wait_for_element("#nested").click()
dash_duo.wait_for_text_to_equal("#nested-output", "Clicked 1 times")

0 comments on commit f7f8fb4

Please sign in to comment.