📣 Introducing Dash `/pages` - A Dash 2.x Feature Preview

project folder structure
A snapshot of my project folder structure (I’m using PyCharm).
The bottom of my project folder (shows the pages plugin file):
rest of project folder

This looks to be an awesome upcoming addition to Dash. Quick question, is there a way to get this to work with long_callback or some form of flask-caching since that is still bound to app? I’m guessing you would need to go back to having both an index.py and app.py file to avoid the circular imports?

2 Likes

Hi @bigmike

It’s true that @callback does not yet support long_callback.

Here’s one workaround I tried: I put the layouts that include the long_callbacks in pages/ and the @app.long_callback s in the app.py file.

It makes the index.py file unnecessary and all the new functionality of Dash pages/ still worked.

@chriddyp is there a better way?

2 Likes

That’s probably the best way for now. The ultimate solution is to have another form of long_callback that doesn’t require the app object.

Would love if this feature was included in the 2.1 release as well (plus support for even more than just one layer deep as well if possible because I have a pretty massive application that I’m trying to convert to Dash and it currently has 3 levels of pages and the additional hierarchy would be very much appreciated).

Hey @dash-beginner - I’m glad you like this feature, and I hope this will be ready for Dash 2.1, but in the meantime it’s available in Dash Labs. See more info here: 📣 Dash Labs 1.0.0: Dash `pages` - An easier way to make multi-page apps

The nested folders feature is available in Dash-Labs. If you get a chance to try it, please let us know how it works for you. It will be faster to make fixes and enhancements in Dash Labs rather than wait for the official Dash release schedule.

3 Likes

This solved my issue. Really appreciate the reply!

1 Like

:mega: I’m pleased to announce the release of Dash Labs V1.0.1

pip install dash-labs -U

This release includes two bug fixes:

  • #59 Fixed bug that prevented order prop from changing the order of the modules in dash.page_registry
  • #55 Fixed bug that prevented multipage apps from working in windows

Big thanks to @johnkangw for reporting the Windows bug and for doing the pull request to fix it! :medal_sports:

See more about how to use this feature in Dash Labs here: 📣 Dash Labs 1.0.0: Dash `pages` - An easier way to make multi-page apps

1 Like

If a page needs a static asset like an image in the layout, how would you recommend doing this when the asset is in app’s assets folder?

i.e. replacing the following:

html.Img(id=“page_icon”, src=app.get_asset_url(“page_icon.png”))

Hi @jgaewsky and welcome to the Dash community :slight_smile:

The assets folder works with pages/. You can see an example of images added to an app in dash-labs here: dash-labs/10-MultiPageDashApp-MetaTags.md at main · plotly/dash-labs · GitHub

See the demo app here: dash-labs/docs/demos/multi_page_meta_tags at main · plotly/dash-labs · GitHub

Thanks, @chriddyp and @AnnMarieW! The pages feature is an excellent addition and makes Dash even better!

I was wondering if it is possible to set up an app structure that allows multiple sub pages per page. The idea is to navigate the pages using a navigation bar at the top, and to add a second navigation bar whenever a page has sub pages. I came up with the following app structure and would like to ask whether you think this is a reasonable approach. For each of the pages, I have a folder containing page-specific stuff: layout, callbacks etc. In my example, page 2 has sub pages:

-- pages
   |-- home
       |-- home.py
   |-- page_1
       |-- page_1.py
   |-- page_2
       |-- sub_item_1
            |-- sub_item_1.py
       |-- sub_item_2
            |-- sub_item_2.py
       |-- submenu.py
       |-- page_2.py
   |-- other pages...
   |-- app.py

In principle, I got this to work with the following implementation, but I am not sure if this is the best approach and would appreciate any kind of feedback. I only add navigation links to the navigation bar at the top when ‘sub’ is not a sub string of the ‘module’ value of a page.

app.py:

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc

from dash import html, Input, Output, State

app = dash.Dash( __name__, plugins=[dl.plugins.pages], external_stylesheets=[dbc.themes.BOOTSTRAP])

navbar = dbc.Navbar(
    dbc.Container([
        html.A(dbc.Row(
            dbc.Col(dbc.NavbarBrand("MyApp", className="ms-2")),
                align="center",
                className="g-0",
        ),
        href="/",
        style={"textDecoration": "none"},
        ),
        dbc.Row([
            dbc.NavbarToggler(id="navbar-toggler"),
            dbc.Collapse(
                dbc.Nav([
                    dbc.NavItem(dbc.NavLink(page['name'], href=page['path'], active="exact")) for page in dash.page_registry.values() if page["module"] != "pages.not_found_404" and "sub" not in page["module"]
                ],
                className="w-100",
                ),
            id="navbar-collapse",
            is_open=False,
            navbar=True,
            ),
        ],
        className="flex-grow-1",
        ),
    ],
    fluid=True,
    ),
    dark=True,
    color="dark",
    fixed="top",
    sticky=True
)

app.layout = html.Div([navbar, dl.plugins.page_container]) 

@app.callback(
    Output("navbar-collapse", "is_open"),
    [Input("navbar-toggler", "n_clicks")],
    [State("navbar-collapse", "is_open")],
)
def toggle_navbar_collapse(n, is_open):
    if n:
        return not is_open
    return is_open

if __name__ == "__main__":
    app.run_server(debug=True)

home.py:

import dash
from dash import html
import dash_bootstrap_components as dbc

dash.register_page(__name__, path="/", order=1)

content = [html.H2("Home")]

layout = html.Div([
   dbc.Row(
       dbc.Col(content, width={"size": 8, "offset": 1}),
       align="center"
   )
])

page_2.py:

import dash
from dash import dcc, html
from .submenu import sidebar

dash.register_page(__name__, path="/page_2", order=2)

content = html.Div([html.H2("Page 2")])

layout = html.Div([sidebar, content])

Creating the submenu automatically from the page registry does not work, so I am creating it manually. I assume, page 2 is not fully registered here, but creating it in app.py and importing it from there is not possible because it would be a circular import.
submenu.py:

import dash_bootstrap_components as dbc
from dash import html

sidebar = html.Div([
    dbc.Nav([  # this works
        dbc.NavLink("Sub Item 1", href="/page_2/item_1", active="partial"),
        dbc.NavLink("Sub Item 2", href="/page_2/item_2", active="partial")
    ], vertical=True, pills=True),

    # dbc.Nav([  # this does not work
    #     dbc.NavLink(page['name'], href=page['path'], active="exact") for page in dash.page_registry.values() if page["module"] != "pages.not_found_404" and "page_2.sub" in page["module"]
    # ], vertical=True, pills=True)


], style={"position": "fixed", "padding": "1rem 1rem", "background-color": "#f8f9fa", "height": "100vh"})

sub_item_1.py:

import dash
from dash import html
from ..submenu import sidebar

dash.register_page(__name__, path="/page_2/item_1", order=1)

content = html.Div([html.H2("Page 2 - Item 1")])

layout = html.Div([sidebar, content])

sub_item_2.py:

import dash
from dash import html
from ..submenu import sidebar

dash.register_page(__name__, path="/page_2/item_2", order=1)

content = html.Div([html.H2("Page 2 - Item 2")])

layout = html.Div([sidebar, content])

With this setup, the ‘active’ style of the dbc.NavLinks in the navigation bar at the top does not work any more. Any ideas on how to use this feature? Any feedback would be highly appreciated!

1 Like

Hey @mawe I’m glad you like pages/ :slight_smile:

Yes, it’s possible to have nested folders within the pages folder. You can find an example here: dash-labs/09-MultiPageDashApp-NestedFolders.md at main · plotly/dash-labs · GitHub

I think your approach for adding the navigation to the top bar is good, and you found a nice workaround for having a conditional side-bar per page. It’s true that in this case it’s too early to use the conveinence of looping through the dash.page_registry because it hasn’t finished building yet. Adding the links directly is a good solution.

Setting active="partial" instead of active="exact" in the header NavLink should make the active style work. However, you probably need to have a different path for the Home page because the current home page path of "/" will show as active if you set it to active="partial"

4 Likes

This piece of the pages/ design concerns me a bit. I wonder how we could get around this.

One idea: what if dash layouts could accept a function and that function would be called later upon serialization and whatever would be returned would be serialized. So you could have like:

header = lambda: [dcc.Link(p) for p in dash.page_registry]
1 Like

Hey @mawe

@chriddyp had a good idea. I changed the layouts and the sidebar to functions and it worked :partying_face:

Could you give this a try?

This is the submenu.py. The sidebar is now a function:

import dash_bootstrap_components as dbc
import dash

def make_sidebar():
    return dbc.Nav([  
          dbc.NavLink(page['name'], href=page['path'], active="exact") for page in dash.page_registry.values() if  "page_2.sub" in page["module"]
      ], vertical=True, pills=True)

This is page2.py
(sub_item_1.py and sub_item_2.py are similar. Import the make_sidebar function, and add it to the layout. Note that the layout is also now a function)

import dash
from dash import html
from .submenu import make_sidebar

dash.register_page(__name__, path="/page_2", order=2)

content = html.Div([html.H2("Page 2")])

def layout():
    return html.Div([make_sidebar(), content])


4 Likes

Hi @AnnMarieW!

Your solution for the sidebar navigation works beautifully!

Also, I got the active style in the main navigation bar at the top to work by simply checking the current page in app.py and setting the active style accordingly:

dbc.NavItem(dbc.NavLink(page['name'], href=page['path'], active="exact" if page["path"] == "/" else "partial")) for page in dash.page_registry.values() if page["module"] != "pages.not_found_404" and "sub" not in page["module"]
4 Likes

@mawe - Super, glad it worked for you :confetti_ball:
This will be a good example to include in the dash-labs docs too.

1 Like

Hi Ann, after reading all the posts, I am wondering if you can help me undrstand the difference in creating multiple dash apps and putting them through flask, versus the multi-page approach above. I see a lot of people are pushing their apps through flasj, what benefits and advantages are theit to this approach. This would be very helpfull, because it would require a lot of fundamental changes to go from one to the other. Best Derek.

Hi @snowde

I haven’t made a multi-page app using flask and multiple apps so I can’t speak to the pros and cons. (If someone else has, please chime in).

However, this is from the Dash 2.0 announcement:

@dash.callback will not work if your project has multiple app declarations. Some members of the community have used this pattern to create multi-page apps instead of the official dcc.Location multi-page app solution.. The multi-app pattern was never officially documented or supported by our team.

We built and officially support the dcc.Location method of multiple pages vs multiple flask instances for a couple of reasons:

  • “Single page app (SPA)” links with dcc.Link: This allows page navigation without reloading the browser page (and therefore reloading and re-evaluating the JS scripts and CSS), making page navigation quite a bit faster
  • Ability to share common components in the “frame” of the page rather than redefining within each page like headers and sidebars
  • Ability to share data like dcc.Store
  • More easily use query parameters in dash callbacks
  • “It’s just Dash” - dcc.Location and dcc.Link provide a multi page app experience using the same simple foundational principles of dash: Rich components tied together with callback functions

Now with /pages, we’re adding even more functionality out of the box (see original post) that you would otherwise need to program from scratch using the flask method.

3 Likes

Thanks, Chris that makes this method really appealing, but I must ask if you could also comment on the possible benefits of using the Flask method, for example, I have read that it is useful for user authentication. Happy holidays.