Create nested multipage app with a unique layout for each directory in /pages

I am hoping there is a way to create a couple nested pages where each directory has it’s own layout that is shared across all files in that directory.

- app.py
- pages
    - home.py
    - students
        |-- demographics.py
        |-- grades.py
        |-- attendance.py
    - behavior
        |-- incidents.py
        |-- dempgraphics.py

I have my main layout in app.py, which is pretty much just a dbc.Navbar. My home.py will have several general graphs. Ideally, I would like to have nested layout in the students directory that is shared across all the .py in that folder. Essentially, the same way the layout in app.py is shared across all pages but just for the files nested under /pages/students

Something like below where /pages/students/layout.py inherits the layout of app.py and is shared across all the files in /pages/students That way I can have all the callbacks in my /pages/students/layout.py and just have the graphs in say /pages/students/grades.py

- app.py
- pages
    - home.py
    - students
        |-- layout.py
        |-- demographics.py
        |-- grades.py
        |-- attendance.py
    - behavior
        |-- incidents.py
        |-- dempgraphics.py

Is that possible without having the layout.py (which is pretty much an AG Grid) reloaded when navigating between the nested pages in /pages/students? I would really like to avoid having both AG Grids for the students and behavior pages in app.py and just hidden based on the page location.

Hey @PyGuy, I think example 7 and 7b might be interesting. I might be wrong, but we had a similar topic not too long ago where these examples were helpful.

Thank you @AIMPED, I did see that and tried it but the layout is loaded every time you navigate to a nested page, which is not ideal for my situation since the AG Grid filters, even with persistence=True, sometimes get reset when navigating between pages and the user loses the filters that they had in place.

I suppose I could save the state of the AG Grid and use that but I am not sure that is really going to be much better than just adding both AG Grids to app.py and setting {display: none;} depending on the page location.

Hi @PyGuy

Setting {display:none} on certain components can be a good workaround. You might also be interested in @Emil 's solution in Dash Exntesions

Another option would be to have only one page for each nested layout, then control the content of the sub pages with a callback on the page.

For example, in the student’s page you could have the layout including buttons (styled as links) for “navigation” to the other content - perhaps something like:

students/layout.py


from .demographics import layout_demographics
from .grades import layout_grades

dash.register_page(__name__)

layout = html.Div(
  [
      html.Button("demographics", id="demographics"),
      html.Button("grades", id="grades"),
      aggrid,
      html.Div(id="other-content")
  ]
)

@callback(
  Output("other-content", "children"),
  Input ("demographics", "n_clicks"),
  Input("grades", "n_clicks")
)
def update(*_):
   # return either layout_demographics or layout_grades
    




1 Like

If you create the shared layout blocks as page components, and include them on the respective pages, I believe you should achieve the desired behavior. In essence, it’s just a syntatically leaner way to toggling visibility (e.g. setting {display:none}) depending on the URL :slight_smile:

2 Likes

@Emil thanks, this seems like the way to go but I keep getting

dash.exceptions.DuplicateIdError: Duplicate component id found in the initial layout: ‘e3e70682-c209-4cac-629f-6fbed82c07cd_wrapper’

I am working on creating a MRE.

Did you add the component to the layout yourself (the component is injected into the layout automatically, so you shouldn’t add explicitly. Doing so would yield that kind of error)?

1 Like

@Emil No, just doing page_components=[table]

UPDATE

The issue is with using a function for the layout in app.py for a dynamic layout on every page load. When I remove the app_layout function and just do:

app.layout = html.Div([
        html.H1('Hello World'),
        # the other .py files under /pages will render here
        page_container,
        # page components will appear here
        setup_page_components()
    ])

it works.

directory

- app.py
- pages
    |-- components.py
    |-- page1.py

app.py

import dash_bootstrap_components as dbc
from dash_extensions.pages import setup_page_components
from dash import Dash, page_container, html

# Dash App
external_stylesheets = [dbc.themes.ZEPHYR]
app = Dash(__name__, external_stylesheets=external_stylesheets,
           title='Dashboard', use_pages=True)


# html page layout
def app_layout():
    layout = html.Div([
        html.H1('Hello World'),
        # the other .py files under /pages will render here
        page_container,
        # page components will appear here
        setup_page_components()
    ])

    return layout


app.layout = app_layout

if __name__ == '__main__':
    app.run(debug=True)

page1.py

from pages.components import table
from dash import register_page, html

register_page(__name__, path='/page1', page_components=[table])


def layout():
    return html.H2('Page 1')

components.py

import dash_bootstrap_components as dbc
import dash_ag_grid as dag
from dash import html

table = dbc.Container([
    html.Div([
        # download button
        dbc.Button('Download CSV', id='csv-button', n_clicks=0,
                   className='me-1'),
        # clear filters button
        dbc.Button('Clear Filters', id='clear-btn', n_clicks=0,
                   className='me-1'),
        # Here is the data table
        dag.AgGrid(id='datatable',
                   className='ag-theme-quartz',
                   columnSize='autoSize',
                   defaultColDef={'resizable': True,
                                  'sortable': True,
                                  'filter': True,
                                  'filterParams': {'buttons': ['reset', 'apply'],
                                                   'closeOnApply': True},
                                  'floatingFilter': True},
                   dashGridOptions={'pagination': True, 'rowSelection': 'single',
                                    'animateRows': True},
                   persistence=True,  # persistence keeps the filters in place on page refresh
                   persisted_props=['filterModel']
                   ),
    ]),
], fluid=True)
1 Like