plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.
https://plotly.com/dash
MIT License
21.43k stars 2.07k forks source link

[Feature Request] Allow multiple top-level components in app layout #1620

Closed Marc-Andre-Rivet closed 3 months ago

Marc-Andre-Rivet commented 3 years ago

Oftentimes when writing an app, a wrapper div is required. Suggesting to make the top-level wrapper optional / implicit and make the layout support a tuple of components.

This:

app.layout = html.Div([
    dcc.Slider(id='slider'),
    dcc.Input(id='input')
])

Becomes:

app.layout = dcc.Slider(id='slider'), dcc.Input(id='input')
alexcjohnson commented 3 years ago

Also mentioned in https://github.com/plotly/dash-core/issues/197 and https://github.com/plotly/streambed/issues/15781 - this will be easy to do on the back end once the core packages are all merged into one, or we could do it in the renderer but that may take more effort (and would still require back-end tweaks to accept a list of components as the layout)

Marc-Andre-Rivet commented 3 years ago

this will be easy to do on the back end once the core packages are all merged into one

I wonder what about the core packages merge would make this easier. Seems that as long as the renderer knows how to handle, visit, and render a list entry point it would be fine? Wrap it in a React Fragment at render time.

alexcjohnson commented 3 years ago

When the packages are merged, dash can just wrap the array in html.Div on its own. I suppose pre-merge we could technically still do this, as dash requires dash-html-components, but it's inching up on a circular dependency.

We could certainly handle this in the renderer natively, but it would require a bunch of extra work to ensure paths are calculated and handled correctly - whereas the back-end change would be trivial.

Marc-Andre-Rivet commented 3 years ago

@alexcjohnson I think supporting this in the renderer (not considering the BE changes as they would need to happen anyway) requires only this:

Add the following in /dash-renderer/src/index.js:

const RendererFragment = ({ children }) => (<Fragment>{children}</Fragment>)

export { RendererFragment };

Modify this section (https://github.com/plotly/dash/blob/dev/dash-renderer/src/APIController.react.js#L133-L142) of /dash-renderer/src/APIController.react.js to

        if (isEmpty(layout)) {
            /* Patch Layout */
            const patchedLayout = Array.isArray(layoutRequest.content) ? {
                namespace: 'dash_renderer',
                type: 'RendererFragment',
                props: {
                    children: layoutRequest.content
                }
            } : layoutRequest.content;

            const finalLayout = applyPersistence(
                patchedLayout,
                dispatch
            );
            dispatch(
                setPaths(computePaths(finalLayout, [], null, events.current))
            );
            dispatch(setLayout(finalLayout));
        }

Disabling part of the BE validation and tweaking it so it accepts a list | tuple layout, this works fine:

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

slider_props = dict(min=0, max=20, step=1, value=5)

app = dash.Dash(__name__)

app.layout = dcc.Slider(id='slider', **slider_props), dcc.Input(id='input'),

@app.callback(
    Output('input', 'value'),
    Input('slider', 'value')
)
def update(value):
    return value

app.run_server(debug=True)

image (screenshot after the callback was triggered)

Since we always determine at runtime the path to components, the layout provided by the BE doesn't really matter I believe, just what gets injected into the FE layout. And here the renderer can inject its own component as need be and have no dependency on how we package things up.

Marc-Andre-Rivet commented 3 years ago

In fact there's no need for

const RendererFragment = ({ children }) => (<Fragment>{children}</Fragment>)

export { RendererFragment };

hooking up directly to React.Fragment works just fine too

            const patchedLayout = Array.isArray(layoutRequest.content) ? {
                namespace: 'React',
                type: 'Fragment',
                props: {
                    children: layoutRequest.content
                }
            } : layoutRequest.content;

although it would be a bit less robust, as changes in React would be affecting us

gvwilson commented 3 months ago

Hi - we are tidying up stale issues and PRs in Plotly's public repositories so that we can focus on things that are most important to our community. If this issue is still a concern, please add a comment letting us know what recent version of our software you've checked it with so that I can reopen it and add it to our backlog. (Please note that we will give priority to reports that include a short reproducible example.) If you'd like to submit a PR, we'd be happy to prioritize a review, and if it's a request for tech support, please post in our community forum. Thank you - @gvwilson