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

Thought about this more tonight. Perhaps a syntax like:

register_page(
    path='/movies/{id}'
)

def layout(id=None):
    return 'You have selected movie {id}'

It could work with multiple levels of nesting:

register_page(
    path='/movies/{country}/{city}'
)

def layout(country=None, city=None):
    return f'You have selected {country} and {city}'
4 Likes

This is great thanks. Does that mean we would not be able to have URLs that would open a specifric tab on a webpage, looking at the query string support.

@snowde You should be able to open specific tabs… Have you tried it with the plug-in it and it did’t work? can you give an example of how you are doing this currently?

Here’s a new feature request:

This plug-in doesn’t yet support pages in nested folders in pages/ . For example, if you have this structure in pages:

- pages
   |-- home.py
   -chapter1
     |-- page1.py
     |-- page2.py
   -chapter2
     |-- page1.py
     |-- page2.py

I’m not sure about the best way to handle this, but it might be convenient if dash.page_registry would also have a folder variable that would be populated automatically. For the example above “pages.chapter1.app1” would also have folder=“chapter1”.

(Note – in this potential solution, the dash.page_registry dict is flat, with a key for each app in pages/)

Then if you were populating submenus for the app, you could create them like this:

html.Div(
    [
        html.Div(dcc.Link(page["name"], href=page["path"]))
        for page in dash.page_registry
        if page["folder"] == "chapter1"
    ]
)

Does anyone have any thoughts or see a better way to do this?

1 Like

Looks great! It looks similar to this project which creates a template of a multipage dash app https://github.com/ned2/slapdash

That project hasn’t been updated for around 2 years, so doesn’t contain the new features of dash 2. Would be really cool to have this new multipage code in a batteries included template. Is there something like this for dash 2 yet?

2 Likes

This is really exciting and definitely reduces the amount of steps needed to build a multi-page app. I took @Emil 's cool Burger component and built a very simple example app.

One area I wonder whether we can enhance is sharing data between pages. Now I’m using dcc.Store and callbacks, but perhaps there are quicker/simpler ways. @AnnMarieW and I were talking and she was thinking maybe we could do it through the dash.register.pages – make it so a dcc.Store is generated automatically through variable name or id.

This app has two pages and the main app.py. See structure below:

home.py (Meat)

import dash
from dash import html, dcc, callback, Output, Input
import pandas as pd
import plotly.express as px

df = pd.read_csv(
    "https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv"
)
print(df.head())

dash.register_page(
    __name__,
    path="/",
    name="Meat",
    description="Welcome to my app",
    order=0,
    icon="fa fa-ambulance",
)

layout = html.Main(
    children=[
        html.H1("US Meat Exports (2011)"),
        html.Div(
            dcc.Dropdown(
                id="state-slctd",
                multi=False,
                value=df.state[4],
                options=[{"label": x, "value": x} for x in df.state],
                style={"width": "60%", "color": "#000000"},
                persistence_type="session",
                persistence=True,
            ),
            style={"display": "flex", "justifyContent": "center"},
        ),
        html.Div(
            id="graph-placeholder",
            style={"display": "flex", "justifyContent": "center"},
        ),
    ],
    style={"width": "100%", "height": "100vh"},
)


@callback(
    Output("graph-placeholder", "children"),
    Output("stored-state", "data"),
    Input("state-slctd", "value"),
)
def update_graph(selected):
    dff = df[df.state == selected]
    fig = px.bar(dff, x=["beef", "pork", "poultry"])
    return dcc.Graph(figure=fig, style={"width": "60%"}), selected

agri.py (Agriculture)

import dash
from dash import html, dcc, callback, Input, Output
import pandas as pd
import plotly.express as px

df = pd.read_csv(
    "https://gist.githubusercontent.com/chriddyp/c78bf172206ce24f77d6363a2d754b59/raw/c353e8ef842413cae56ae3920b8fd78468aa4cb2/usa-agricultural-exports-2011.csv"
)

dash.register_page(
    __name__, name="Agriculture", path="/agriculture", icon="fa fa-fw fa-heart-o"
)


def layout():
    return html.Main(
        [
            html.H1("US Agricultural Exports (2011)"),
            html.Div(
                id="graph-placeholder2",
                style={"display": "flex", "justifyContent": "center"},
            ),
        ],
        style={"width": "100%", "height": "100vh"},
    )


@callback(
    Output("graph-placeholder2", "children"),
    Input("stored-state", "data"),
)
def update_graph2(selected):
    print(selected)
    dff = df[df.state == selected]
    fig = px.bar(dff, x=["corn", "wheat", "cotton"], title=f"{selected}")
    return dcc.Graph(figure=fig, style={"width": "60%"})

And the main app.py

from dash import Dash, html, dcc
import dash
import pages_plugin
from dash_extensions import Burger

# Example CSS from the original demo.
external_css = [
    "https://negomi.github.io/react-burger-menu/example.css",
    "https://negomi.github.io/react-burger-menu/normalize.css",
    "https://negomi.github.io/react-burger-menu/fonts/font-awesome-4.2.0/css/font-awesome.min.css",
]

app = Dash(__name__, plugins=[pages_plugin], external_stylesheets=external_css)
# print(list(dash.page_registry.values())[1]['icon'])


app.layout = html.Div(
    [
        Burger(
            children=[
                html.Nav(
                    children=[
                        dcc.Link(
                            [html.I(className=page["icon"]), html.Span(page["name"])],
                            href=page["path"],
                            className="bm-item",
                            style={"display": "block"},
                        )
                        for page in dash.page_registry.values()
                    ],
                    className="bm-item-list",
                    style={"height": "100%"},
                )
            ]
        ),
        pages_plugin.page_container,
        dcc.Store(id="stored-state"),
    ],
    style={"height": "100%"},
)


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

2 Likes

Hey @adamschroeder

Thanks for posting another nice example. A few cool things here:

  • This shows how to have names for the pages that are different than your file names, and how to include other page specific data such as icons:

For example agri.py sets the name “Agriculture” and adds the icon here:

dash.register_page(
    __name__, name="Agriculture", path="/agriculture", icon="fa fa-fw fa-heart-o"
)

and in app.py it’s used to set the label on the link:

dcc.Link( [html.I(className=page["icon"]), html.Span(page["name"])], ....
  • Nice use of @Emil 's burger menu!

  • For dash-bootstrap-component users, you can also use the new dbc.Offcanvas component – new in V1.0. See more info here and it’s now easier to add icons

2 Likes

2 posts were split to a new topic: Attempting to connect a callback Input item but no component with that ID exists

Great new feature! Am working with this, but am coming across a weird error. I’m trying to build a landing page in a seperate file, but its not picking up the full list of pages that are registered.

I assume this is something to do with the order in which register_page is called - so when layout() is run in my file the page_registry doesn’t have all of the pages registered yet?

Do you have to have the landing page defined in app.py? When its in app.py it seems to work perfectly…

Or is it something to do with the order=0 parameter in register_page?

Regards,
Amien

Hi @pandamodium

It’s possible to build the landing page in a separate file. Can you say more about how you have things set up?

You can also find some examples here

Hey Ann!

Thanks for replying! Its a basic app with a few pages in a “pages” folder.

I put the below code in pages/home.py - but the links are only for the first few pages. It looks like it runs in alphabetical order, but only up to the page in question.

app.layout = html.Div(
    [
        html.H1("App Frame"),
        html.Div(
            dcc.Link("Go back home", href=dash.page_registry["pages.home"]["path"])
        ),
        html.Div(
            [
                html.Div(
                    dcc.Link(f"{page['name']} - {page['path']}", href=page["path"])
                )
                for page in dash.page_registry.values()
                if page["module"] != "pages.not_found_404"
            ]
        ),
        dl.plugins.page_container,
    ]
)

Hi @pandamodium

There is still not enough info in your post for me to help. The pages are all registered when the app starts, so timing shouldn’t be an issue.

Here are a couple debugging tips:

You can use your IDE’s debugger tool to inspect dash.page_registry or you can do it the “hard way” and print it.

if you simply use: print(dash.page_registry), you may get something that’s pretty hard to read. To print it in a nicer format, , I like to remove the layout key in the nested dict and format it with json.dumps Here’s an example:

Note - Dash pages is now available in dash-labs v1.0.0 as a plug-in. (pip install dash-labs)

import dash_labs as dl

app = Dash(__name__, plugins=[dl.plugins.pages])

# ---  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))
#  ------------------------------------

...

Is the page you are looking for not in dash.page_registry? How did you define the dash.register_page in home.py? Adding path="/" will make it the landing page:

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

Thanks AnnMarieW!

I put that code in - and was still getting the same effect, but I think it worked it out!

The solution was to make sure the layout definition was a function. so my home.py becomes:

def layout():
	return  dbc.Container([
	html.Div(
		dcc.Link('Go back home', href=dash.page_registry['pages.home']['path'])
	),

	html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),
])

It seems that if you have the home page in a separate page - it will generate the layout objects in the python files in the “pages” folder when you start (in alphabetical order), and so if your file is called home.py, nothing after H will be in the page_registry at the time that code is run.

Anyway if anyone else has this issue - just remember to use a function for the layout object.

Regards,
Amien

@pandamodium

It’s not supposed to work that way, so this might be a bug. You should be able to define any of the pages to be the home page without having to make the layout a function.

The code I provided was to help with debugging and not to fix it. It would be very helpful if you shared what was printed and how you had dash.register_page(...) defined in each of the apps in the pages/

Hey Ann!

Sure thing - see below for a stripped down version showing the behaviour in practice:

image

pages/home.py:

import dash
from dash import Dash, html, dcc,callback, Input, Output, State	
import dash_bootstrap_components as dbc

# ---  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))
#  ------------------------------------

dash.register_page(
	__name__,
	path='/',
	name='Old Landing',
	description='Welcome to Athena',
	order=0,
	redirect_from=['/old-home-page', '/v2'],
	extra_template_stuff='yup'
)

layout = dbc.Container([
	html.Div(
		dcc.Link('Go back home', href=dash.page_registry['pages.home']['path'])
	),

	html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),
])

app.py:

from dash import Dash, html, dcc
import dash
import pages_plugin

app = Dash(__name__, plugins=[pages_plugin])


dash.register_page('another_page', layout='Another page', path='/another-page')
dash.register_page('and_again', layout='And again!', path='/and-again')


app.layout = html.Div([
	html.H1('App Frame'),

	pages_plugin.page_container

])



if __name__ == '__main__':
	app.run_server(debug=True,port=8111)

The result from running python app.py:
image

Apologies and the logging output was:

Dash is running on http://127.0.0.1:8111/

 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
{
    "pages.analyticapps": {
        "module": "pages.analyticapps",
        "supplied_path": "/oldhome",
        "path": "/oldhome",
        "supplied_name": "Analytic Apps",
        "name": "Analytic Apps",
        "supplied_title": null,
        "title": "Analytic Apps",
        "supplied_description": "Welcome to my app",
        "description": "Welcome to my app",
        "supplied_order": 0,
        "supplied_layout": null,
        "extra_template_stuff": "yup",
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    },
    "pages.historical_archive": {
        "module": "pages.historical_archive",
        "supplied_path": null,
        "path": "/historical-archive",
        "supplied_name": null,
        "name": "Historical archive",
        "supplied_title": null,
        "title": "Historical archive",
        "supplied_description": null,
        "description": "Historical archive",
        "supplied_order": null,
        "supplied_layout": null,
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    }
}
{
    "pages.analyticapps": {
        "module": "pages.analyticapps",
        "supplied_path": "/oldhome",
        "path": "/oldhome",
        "supplied_name": "Analytic Apps",
        "name": "Analytic Apps",
        "supplied_title": null,
        "title": "Analytic Apps",
        "supplied_description": "Welcome to my app",
        "description": "Welcome to my app",
        "supplied_order": 0,
        "supplied_layout": null,
        "extra_template_stuff": "yup",
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    },
    "pages.historical_archive": {
        "module": "pages.historical_archive",
        "supplied_path": null,
        "path": "/historical-archive",
        "supplied_name": null,
        "name": "Historical archive",
        "supplied_title": null,
        "title": "Historical archive",
        "supplied_description": null,
        "description": "Historical archive",
        "supplied_order": null,
        "supplied_layout": null,
        "image": "app.jpeg",
        "supplied_image": null,
        "redirect_from": null
    }
}

@pandamodium
Thanks so much for sharing the output, I think see what’s going on now.

The links should be populated in app.py:

html.Div([
		html.Div(dcc.Link(
			f"{page['name']} - {page['path']}",
			href=page['path']
		))
		for page in dash.page_registry.values()
		if page['module'] != 'pages.not_found_404'
	]),

And this bit of debugging code below should go right before the layout in app.py. If you put it in one of the apps in pages/, it will only show what’s been added up to that point.

To confirm my theory, could you give it a try? It’s also odd that "pages.analyticapps" shows up twice in the dict output you provided.

# ---  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))
#  ------------------------------------

Hey Ann!

Yes that’s exactly what happens!

Unless you make the Layout in home.py a function, in which case it updates the layout each time it is called and populates the full list.

And I think analytics app is being repeated because I have debug=True, I’ve noticed it duplicates all my logging stuff if I have that on.

Will paste the logs later this morning for you to review.

Thanks again for all your help!

Regards
Amien

2 Likes

Hi all,
This is so timely! I’m working on a multi-page app that takes into consideration our physical and labor capacity and need to show different views. I was struggling to try to figure out a way to use the tabs and make it work. I’ve gotten a few pages stitched together and noticed one thing.

I have persistence turned on for many of my inputs but I noticed that whenever I return back to that page the data is not storing. I have persistence turned on and it worked previously, but now that I’m using a multi page app the data is not persisting when I switch back to the link. Is there a way to resolve this issue or is it a bug?

dcc.Dropdown(
        id='auction_dropdown',
        options=[{'label': auction, 'value': auction}
                 for auction in ['AAAW', 'FAAO', 'SVAA', 'MAA', 'BWAE', 'GCAA', 'PXAA', 'DFWA', 'BCAA', 'GOAA']],
        value=['AAAW', 'FAAO', 'SVAA', 'MAA', 'BWAE', 'GCAA', 'PXAA', 'DFWA', 'BCAA', 'GOAA'],  # Default value to show
        persistence=True,
        multi=True,
        searchable=False
    ),

Hi @johnkangw

Welcome to the Dash community and thanks for trying the new pages plug-in :slight_smile:

I just tried this and it worked fine for me – selections persisted when changing pages. I just copied your dropdown into one of the apps in the pages folder in this dash-labs example.

Can you provide a minimal example that replicates the issue?