Dash Pages - Multi-Page App with Subtabs using dbc.NavLinks

I have an app I’ve been working on for a while. Prior to the release of Pages, it was structured as a multi-page app using the ‘old way’ using dcc.Tabs. More specifically, ‘app.py’ creates app instance and imports all of the other pages (e.g. hello.py, world.py, etc), all of which had a ‘layout’ variable. I then used a callback to determine which layout to display. E.g. (in app.py):

@app.callback(Output('tabs-content', 'children'),
                [Input('tabs', 'value')],
)
def render_tab(tab):

    if tab == 'tab-1':
        return hello.layout
    elif tab == 'tab-2':
       return world.layout

The app.py layout is composed of the navigation and a div for the content:

app.layout = html.Div(
                [
                   html.Div(
                        [
                            dcc.Tabs(
                                id='tabs',
                                value='tab-1',
                                children=[
                                    dcc.Tab(label='Hello', value='tab-1'),
                                    dcc.Tab(label='World', value='tab-2')
                                ],
                            ),
                        ],
                    html.Div(id='tabs-content'),
    ])

This all worked fine. However, I wanted to refactor the app to use the new Pages functionality.

So I removed the individual page imports and added added ‘use_pages=True’
to app.py. I also added ‘dash.register_page(name)’, changed @app.callback to @callback, and deleted the “if name == ‘main’: app.run_server(debug=True)” line in each individual page.

However, I wasn’t quite sure how to integrate the dcc.Tabs functionality into Pages. For example, where does dash.page_container go? And how should I modify my tab callback?

Right now my modified layout looks like this:

app.layout = html.Div(
                [
                   html.Div(
                        [
                            dcc.Tabs(
                                id='tabs',
                                value='tab-1',
                                children=[
                                    dcc.Tab(label='Hello', value='tab-1'),
                                    dcc.Tab(label='World', value='tab-2')
                                ],
                            ),
                        ],
                    html.Div(id='tabs-content'),
                    dash.page_container,
    ])

and my modified Tab callback looks like this:

@app.callback(Output('tabs-content', 'children'),
                [Input('tabs', 'value')],
)
def render_tab(tab):

    if tab == 'tab-1':
        return dcc.Location(pathname=dash.page_registry['pages.hello']['path'], id="registry-hello")
    elif tab == 'tab-2':
       return dcc.Location(pathname=dash.page_registry['pages.world']['path'], id="registry-world")

But this does not work. On load, everything seems fine. the Navigation loads and the ‘hello’ page (tab 1) displays just fine. However, when I click on Tab 2 (world.py), it appears to load for a split second, but then the entire app updates and everything resets back to tab 1.

I tried moving dash.page_container inside the ‘tab-content’ div, but that just resulted in no tab displaying. I am convinced (I hope) that this is an easy fix, but I’ve been looking for a couple of hours now and I cannot seem to figure it out.

Thanks for any help!

The page container is where your pages get rendered. Try commenting out your callback and everything related to the tabs

1 Like

Hi @etonblue

Just to expand on @AIMPED’s answer, the callback logic required for URL routing is included with Pages, so you don’t need to included that in your app. When the url is updated, for example when a user clicks on a link, Pages will automatically render the layout for that page in the dash.page_container

The dcc.Tab component doesn’t work well with the Pages navigation (There is a way to update the url in a callback, but this isn’t the “easy” way) . Instead , you could use dcc.Link components and style them to look like tabs. Another solution is to use a dbc.Nav and dbc.NavLink components. You can find examples here: Nav - dbc docs

Here is an example of a nav component that is styled to look like tabs.


dbc.Nav(
    [
        dbc.NavLink(
            html.Div(page["name"], className="ms-2"),
            href=page["path"],
            active="exact",
        )
        for page in dash.page_registry.values()
    ],
    pills=True,    
)



1 Like

Unfortunately my tab structure is a bit more involved. In the actual app, the Navigation area has five tabs, two of which have 3 subtabs. Each tab is a different page, e.g., tab1=page1, subtab2-1=page2, subtab 2-2=page 3, subtab 2-3=page 4, subtab 3=page 5, for a total of 9 separate pages. The layout:

[Tab1] [Tab2] [Tab3] [Tab4] [Tab5]   <- always displayed
 [SubTab2-1][SubTab2-2][Subtab2-3]   <- hidden unless one of two tabs with subtabs is selected
           [Tab Content]

This is easy to do with dcc.Tab and dcc.Tabs.

dcc.Tabs(
     value='tab-1',
     children=[
         dcc.Tab(label='Tab 1', value='tab-1'),
         dcc.Tab(label='Tab 2', value='tab-2', children=[
             dcc.Tabs(id='subtab2', value='subtab2-1', children=[
                 dcc.Tab(label='Subtab 2 - 1', value='subtab2-1'),
                 dcc.Tab(label='Subtab 2 - 2', value='subtab2-2')
             ])
         ]),
         dcc.Tab(label='Tab 3', value='tab-3'),
     ])

etc. etc.

I am not that familiar with dash bootstrap-components as I don’t use it anywhere else in my app, but I could not figure out how to easily re-create my ‘subtab’ layout using dbc.NavBar. I tried mixing and matching dbc.NavBar and dbc.Collapse and some other components, but found myself back in the same situation where I was trying to insert a specific url in a specific place. Am I missing some NavBar functionality? Or is this just a case where I have to choose between my layout or the Pages functionality?

Hello @etonblue,

Is it possible for you to record the way your interactions were in the old index version?

It might help us understand a little more about how your app was working.

You can make the sub tab part of the layout in the pages that have sub-tabs. You can find an example here:

Also, if you prefer to use the tabs, it is still possible to use callbacks for navigation. You can find more examples in the Navitation with Callbaks section . The downside with this method currently is that it refreshes the page when you change the tabs. There is an open pull request to fix this, but it’s not been merged yet.

3 Likes

Okay, I think I can work with this! I have incorporated your example into my app and it is almost working (other than the layout that I will have to recreate). So my folder structure is:

project/
|-----app.py
|---- data/
|---- assets/
|---- pages/
        |---- page1.py
        |---- red-page2.py
        |---- red-page3.py
        |---- red-page4.py
        |---- page5.py
        |---- sub_nav.py

changes to app.py:

app = dash.Dash(__name__, use_pages=True, suppress_callback_exceptions=True)

dbc.Nav(
    [
        dbc.NavLink(page['name'], href=page['path'])
            for page in dash.page_registry.values()
            if page.get('top_nav')
    ],
    pills=True,    
),

additions to the page files that I want displayed at the top level of the menu (page1.py, red-page2.py and page5.py):

from .sub_nav import subnav
dash.register_page(__name__, top_nav=True)

additions to the pages that should be displayed only at the sub_nav level (red-page2.py, red-page3.py, red-page4.py):

from .sub_nav import subnav
dash.register_page(__name_)

addition to the ‘layout’ of all pages in the pages folder:

html.Div(subnav()),

the sub_nav.py file looks like this:

def subnav():
    return html.Div(
        dbc.Nav(
            [
                dbc.NavLink(
                    [
                        html.Div(page["name"], className="ms-2"),
                    ],
                    href=page["path"],
                    active="exact",
                )
                for page in dash.page_registry.values()
                if page["path"].startswith('/red')
                #for page in dash.page_registry.values()
                #if 'red' in page["path"]
            ],
            # vertical=False,
            pills=True,
            className="bg-light",
        )
    )

I have two problems.
First, the last page meeting the sub_nav condition (red-page4.py) doesn’t show up at all. It is set up exactly the same as the second page (red-page3.py).

Second, it appears as if the dash.page_registry is incomplete when the subnav function is called for certain pages. So if I click on the red-page2 top_nav link, I get a link to red-page2 in the sub_nav menu. If I click on the red-page3 sub_nav link, I get a link to red-page2 and red-page3 in the sub_nav menu. What I want is for the sub_nav menu to display all three pages matching the condition in the sub_nav menu.

Any idea why these two things are happening?

To access dash.page_registry from within a file in the pages directory, you’ll need to use it within a function. You can see this in action in the example in the GitHub repo.

For more information about this, see the Dash Page Registry section of the docs:

1 Like

@AnnMarieW thank you for your patience.

I have been closely following the example you initially provided (which appears to be the same example in the Documentation page), which includes utilizing a function to access dash.page_registry.

In my previous post, I have a file in the pages directory (sub_nav.py) that contains a layout function subnav() that returns a NavLink object with all of the pages in dash.page_registry that startwith (‘red’). The subnav() function is imported (from .sub_nav import subnav) and called in the layout of each page.

As I described above, it sorta works, but seems to be missing pages depending on when subnav() is called.This is what I cannot seem to figure out.

For example, If I add your registry debugging code:

# ---  debugging - print `dash.page_registry`
import json
registry = {a:{c:d for c, d in b.items() if c != 'layout'} for a, b in dash.page_registry.items()}
print(json.dumps(registry, indent=4))

to app.py or to the top-level of a page with sub-pages (e.g., red-page1.py), it shows all 9 pages in the dict.

However, if I add the debugging code to the sub_nav function itself, it loads the dict 3 times (presumably once for each time the subnav() function appears in a page layout). The first dict has 5 pages (missing two of the subnav pages), the second dict has 6 pages (missing one of the subnav pages), and the third dict has 7 pages (all of the subnav pages, but missing the two last top_nav pages).

So clearly, when the function is firing, dash.page_registry is not complete despite the fact that I am using a function to access dash.page_registry. Banging my head against my desk at this point. I should note that the only layout that is a function in my app is the one in sub_nav.py.

After reading all 141 pages of the “Introducing Dash Pages” thread on this forum, I finally hit upon the solution (which was, of course, staring me in the face from the example), although I am not exactly sure ‘why’ it works.

All I had to do was change all of the layouts in each of my pages to functions. So changing:

layout = html.Div(
            [

to:

def layout():
    return html.Div(
           [

fixed my issue. Egg on my face, but this is actually in @AnnMarieW’s example: “dash multi page app multi_page_layout_functions”, but I somehow missed it. I was focused on the sub_nav function and not on the layouts.

So its fixed, but I am still not sure I understand why.

1 Like

Glad you got this working! Making the layout a function is they key to making it work, so I’ll try to make it more clear in the example.

The reason it needs to be a function is that the dash.page_registry dict is built as each module in the pages folder is imported by Pages before the app starts. So layout=… is defined at the time the page is imported, and the dash.page_registry is not yet complete. When the layout is a function, it gets called when the app is running and you navigate to that page. At that time the dash.page_registry is complete, so it works :tada:

Well it’s been awhile, but I am back with another directly related question. As mentioned above, my layout is something like:

[Tab1] [Tab2] [Tab3] ← always displayed
[SubTab2-1][SubTab2-2][Subtab2-3] ← hidden unless one of two tabs with subtabs is selected
[Tab Content]

Thanks to @AnnMarieW, I was able to get this to work by making a sub-tab function part of the layout in each of the pages with subtabs. However, this results in the name of the parent tab being repeated as the name of one of the subtabs. For example assume the following tabs, with the Trucks parent tab currently selected:

[Cars] [Trucks] [Planes]
[Trucks][Dump Trucks][Garbage Trucks]
[Trucks Page]

As you can see, the “page” Trucks appears as both a Tab and one of the Subtabs. But I do not want the parent page Trucks to display anything other than the subtabs and the first subtab page. So instead we would have:

[Cars] [Trucks] [Planes]
[Dump Trucks][Garbage Trucks]
[Dump Trucks Page]

Where the Trucks page acts as a container for the two sub-tabs and the first sub-tab (Dump Trucks) opens by default when Trucks is selected.

I’ve tried creating a dummy Truck page with a layout something like:

def layout():
return html.Div(
    [
        /* html.Div(id="hidden", style = {"display": "none"}), */
        html.Div(
            [
                html.Div(
                    [
                        html.Div(subnav_truck_types(), className="tabs"),
                    ],
                    className="bare-container--flex--center twelve columns",
                ),
            ],
            className="row",
        ),
    ],
    id="main-container",
)

where the subnav function looks like:

def subnav_truck_types():
return html.Div(
    dbc.Nav(
        [
            dbc.NavLink(
                page['name'],
                href=page['path'],
                className = 'tab',
                active='partial'
            )
            for page in dash.page_registry.values()
            if page["path"].startswith("/trucks/")
        ],
        className='sub-tabs',
        style={"marginTop": "-40px"}
    )
)

but I couldn’t quite figure out what the output would be to get the first subtab to automatically load. I could get the subtabs to show by adding a “hidden” div, but that clearly isn’t the answer because the subtab pages would only show if they were clicked.

Two questions:

  1. How do I structure this so the first sub-tab page ([Dump Trucks]) displays as default when the parent [Truck] tab is selected; and
  2. where the layout switches to the second sub-tab page ([Garbage Trucks]) when selected?

I am positive that I am just missing something easy and I am looking forward to @AnnMarieW’s one line solution. :slight_smile:

Haha I have a suggestion that’s close to a one-line solution (I hope it works for you :slight_smile: )

You can add arbitrary entries into the dash.page_registry dict to make it easier to loop through to create things like sub menus. For example in the Trucks sup-pages, you can add:

dash.register_page(__name_, sub_menu="trucks")

Then when you loop through to create the sub menu, it would look like:


    dbc.Nav(
        [
            dbc.NavLink(
                page['name'],
                href=page['path'],
                className = 'tab',
                active='partial'
            )
            for page in dash.page_registry.values()
            if page["sub_menu"] == "trucks"
        ]
       

You can see an example multi-page-app-demos. See example #7. The links in the top nav bar are created this way.

If this doesn’t work, I may need to see a little more code. If you could post a minimal example it would help.

Thanks for the quick reply! Unfortunately, its not quite what I am trying to do. My directory structure includes a subdirectory in the pages folder (/pages/trucks/). Inside that folder are two “pages”: dumptruck.py and garbagetruck.py. Using:

if page["path"].startswith("/trucks/")

in my sub_nav function works just fine to display the subtabs. You suggested using an arbitrary entry:

if page["sub_menu"] == "trucks"

Which also works just fine to display the sub-tabs when the parent tab is selected (although to get the arbitrary entry to work, I had to add the variable/value pair to each page (.e.g, sub_menu = “trucks” for the two truck subtabs and sub_menu = “None” to all of the other pages). Otherwise I kept getting key (“sub_menu”) not found errors).

However, neither my solution nor your proposed solution auto selects the first subtab and displays the first subtab page in the container.

Actually, your pages example #7 is a perfect MRE for the problem. The behavior of example #7 is exactly what I have now. That is, when you click on the “Topics” navbar link, three subtabs appear: “Topics” (again), “Topic 2”, and “Topic 3”.

What I want is a version of example #7 where only “Topic 2” and “Topic 3” appear as subtabs when “Topics” is selected, where Topic 2 (the first sub-page) is automatically selected and displayed in the container, and where when I click on Topic 3, Topic 3 replaces Topic 2 in the container.

Is there a way to delineate a specific page as “active” in a layout?

1 Like

Hello @etonblue,

Technically, yes, you could do this.

When registering a page, you can define anything that you want to, as long as the name doesnt interfere.

Here is a callback designed to redirect to the first child:

import dash
from dash import Input, Output, State

import dash_bootstrap_components as dbc


app = dash.Dash(
    __name__,
    use_pages=True,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
)


navbar = dbc.NavbarSimple(
    dbc.Nav(
        [
            dbc.NavLink(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page.get("top_nav")
        ],
    ),
    brand="Multi Page App Demo",
    color="primary",
    dark=True,
    className="mb-2",
)


app.layout = dbc.Container(
    [navbar, dash.page_container],
    fluid=True,
)

@app.callback(
    Output('_pages_location', 'href', allow_duplicate=True),
    Input('redirect_child', 'id'),
    State('_pages_location', 'pathname'),
    prevent_initial_call=True
)
def redirect_child(i, p):
    for page in dash.page_registry.values():
        if page['path'] == p and page.get('top_nav'):
            for pg in dash.page_registry.values():
                if page['path'][1:] == pg.get('submenu') and pg.get('first_child'):
                    return pg['path']
    return dash.no_update


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

topic_2.py

from dash import html

import dash
import dash_bootstrap_components as dbc

from ..side_bar import sidebar

dash.register_page(__name__, first_child=True, submenu='topics')


def layout():
    return dbc.Row(
        [dbc.Col(sidebar(), width=2), dbc.Col(html.Div("Topic 2 content"), width=10)]
    )

topic_1

from dash import html

import dash

dash.register_page(
    __name__,
    name="Topics",
    top_nav=True,
    path='/topics'
)


def layout():
    return html.Div(id='redirect_child')

And I moved topic_2 and topic_3 into a folder “topics” to make the navigation line up with it.


You could probably make this a quicker interaction too by creating key-value dictionary with the mapping. :slight_smile:

example:

import dash
from dash import Input, Output, State

import dash_bootstrap_components as dbc


app = dash.Dash(
    __name__,
    use_pages=True,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
)


navbar = dbc.NavbarSimple(
    dbc.Nav(
        [
            dbc.NavLink(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page.get("top_nav")
        ],
    ),
    brand="Multi Page App Demo",
    color="primary",
    dark=True,
    className="mb-2",
)


app.layout = dbc.Container(
    [navbar, dash.page_container],
    fluid=True,
)

routing_pairs = {}
for page in dash.page_registry.values():
    if page.get('top_nav'):
        for pg in dash.page_registry.values():
            if page['path'][1:] == pg.get('submenu') and pg.get('first_child'):
                routing_pairs[page['path']] = pg['path']

@app.callback(
    Output('_pages_location', 'href', allow_duplicate=True),
    Input('redirect_child', 'id'),
    State('_pages_location', 'pathname'),
    prevent_initial_call=True
)
def redirect_child(i, p):
    return routing_pairs.get(p) or dash.no_update


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

Are you saying that the page for the top menu has no content other than the sub menus? If so, using #7 as an example, you would delete topic_1.py Then topic2.py is used in the top nav bar and just uses the name “Topics” for the nav link

2 Likes

Good gravy that is complicated. Couldn’t we just add a ‘parent’ tag to Pages or something? :upside_down_face:

Thank you @jinnyzor. It took me a while, but I finally got it to work. A couple of notes because it took me a while to adapt it to my situation:

  1. I could not get the key-value dictionary version to work at all. Not sure why.
  2. The ‘submenu’ variable must be the same name as the parent tab.
  3. first-child=True should only be set on the sub page that you want to be default.
  4. you can still use the ‘order’ keyword to determine the order of the subtabs.
  5. the redirect_child callback and function goes in the parent page (which in my case is not my app.py).

@AnnMarieW - I just saw your reply as I was posting this. How do you give a page both a “parent name” (e.g., Trucks) and a subtab name (e.g., Dump Trucks)? I can figure out how to get this:
[Cars] [Trucks ] [Planes]
[Trucks ][Dump Trucks]
[Trucks Page ]

but I can’t figure out how to get this:
[Cars] [Trucks ] [Planes]
[Garbage Trucks ][Dump Trucks]
[Garbage Trucks Page ]

where Trucks and Garbage Trucks refer to the same page.

Note that it’s not necessary to loop through the dash.page_registry to create links - it’s just that it can be super convenient in huge projects with a lot of links and pages. To simplify things, you can start by doing things more “manually” then if you like, you can refactor to add more content to the dash.page_registry if that makes it easier.

Try running example 7 locally. Delete topic_1.py, then change the navbar in app,py to:


navbar = dbc.NavbarSimple(
    dbc.Nav(
        [
            dbc.NavLink("Home", href="/"),
            dbc.NavLink("About", href="/about"),
            dbc.NavLink("Topics", href="/topic-2"),
        ],
    ),
    brand="Multi Page App Demo",
    color="primary",
    dark=True,
    className="mb-2",
)

Man this is fun! So I have now gotten it to work both ways! Thanks to both of you. I went with the easiest solution. It turns out that all I really needed to do was to add the “title” keyword, and then use page[‘title’] to control the display of the parent (‘Truck’) tab. I knew it would be easy!

Process:

  1. delete the “Truck” page;

  2. change the first (default) sub-tab’s registration to:

    dash.register_page(__name__, name = "Garbage Truck", title = "Truck", top_nav=True,  order=4)
    

I used order to ensure that Garbage Truck would appear first. The second sub-tab’s registration became:

dash.register_page(__name__, title = "Dump Truck", order=5)
  1. the subnav function to (simply changed page[‘name’] to page[‘title’] for the display name):

    def subnav_truck_type():
     return html.Div(
       dbc.Nav(
         [
             dbc.NavLink(
                 page['title'],
                 href=page['path'],
                 className = 'tab',
                 active='partial'
             )
             for page in dash.page_registry.values()
             if page["path"].startswith("/trucks/")
         ],
         className='sub-tabs',
         style={"marginTop": "-40px"}
     )
    )
    

This gives:

[Car][Truck][Plane]
[Garbage Truck][Dump Truck]
[Garbage Truck Content]

when truck is initially selected.

I knew it would be easy! Unless I’m missing something.

Thank you both for working through this!

Edit: Turns out I can only mark one solution, so I’m going to give it to @jinnyzor because of how fun his solution was.

2 Likes