šŸ“£ Introducing Dash `/pages` - A Dash 2.x Feature Preview

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.

Latest Dash Labs Docs and Examples:

Based on @vuthanhdatt 's question in this post, see the new example of how to pass parameters to pages using query strings.

Based on the question from @bigmike and @vnnw in this post, there is now an example of how to use @app.long_callback with pages/.

The new chapter on layout functions is based on @mawe’s question on how to make a nav menu for certain pages. Special thanks to @pandamodium for pointing out that it’s necessary to make the layout a function if you are using dash.page_registry from within the `pages/ folder.

Note - This documentation is a work in progress. Please feel free to make suggestions for improvements by commenting here or making a pull request in dash-labs.

Multi-Page App Docs

New in dash-labs>=1.0.0:

Multi-Page App Demos

Examples and demos are located in the docs/demos directory.

  • multi_page_basics
    • Minimal examples of all the features and basic quickstart apps. (see chapter 8 for details.)
  • multi_page_example1
    • A quickstart app using dash-bootstrap-components and some simple callbacks.
  • multi_page_layout_functions
    • An app that creates a sidebar menu for certain pages. (See chapter 11 for details.)
  • multi_page_long_callback
    • An example of how to use @app.long_callback() with pages/
  • multi_page_meta_tags
    • The example app used to show how the meta tags are generated. (See chapter 10 for details.)
  • multi_page_nested_folders
    • This is the example app used in chapter 9.
  • multi_page_query_strings
    • An example of using query strings in the URL to pass parameters from one page to another.

Installation

dash-labs is regularly updated, so be sure to install the latest version:

$ pip install -U dash-labs

New feature coming soon: Variables in the pathname.

3 Likes

Awesome release @AnnMarieW and many thanks to everyone involved!

1 Like

@AnnMarieW Is there a way to incorporate flask decorators in with the /pages functionality? For example, there were a few packages that I was looking at in flask that help you restrict access to certain pages based on user auth status using a decorator - would I need to use the previous url router callback to handle that or do you happen to know of a way to incorporate that into here? Thanks! :slight_smile:

I would love to see this include customizing the meta data based on the variables.

Example:
We might have urls like /news/sports/1 and /news/sports/2. Each of these articles cover different topics and the article title and overview should be displayed when sharing instead of a generic text about sports articles.

@raptorbrad

Now you are asking for the hard stuff! :sweat_smile:

That feature came up during the code review for handling variables in the pathname, but we decided to start with the generic version. I wasn’t sure how popular this would be – but since you brought it up before this was even released, I’ll open an issue on Gitub for the feature request.

Thanks for the suggestion - you can track the progress here.

@raptorbrad Update: Here’s a potential solution . Feedback is welcome :slightly_smiling_face:

I supposed that solution does work, but I don’t think it’s ideal for more dynamic page. I’m not fully aware of how the meta tags work, but a solution I thought of is either:

  1. some meta component defined in the layout that overrides the register_page meta information. OR
  2. an additional return object within the layout function that is a dictionary of meta tags

I’m not sure if either of these are feasible, since I’m not too familiar with what’s going on under the hood.

Another follow-up thought to this thread - would be awesome as multipage apps start to become more supported by Dash if there was an extension to it that tackled the namespace issues discussed in this thread - I find that I sometimes copy and paste code between pages / naturally end up using similar naming schemes in different pages, which muddies up the namespace. Would be awesome if this register page feature was able to incorporate a uuid into each page declaration and prepend it to the callbacks and the id’s of that page (I personally never need a callback to be able to access information from separate pages, but I guess the ability to remove the uuid for spot cases would also be nice if needed in a niche case to access something elsewhere…).

1 Like

I agree on this point, @dash-beginner. Maybe we could do something similar to the implementation in dash-extensions where each page has an optional prefix argument which is prefixed onto the IDs of the involved component and callbacks to avoid ID collisions between pages.

3 Likes