Skip to content

Latest commit

 

History

History
426 lines (315 loc) · 14.3 KB

08-MultiPageDashApp.md

File metadata and controls

426 lines (315 loc) · 14.3 KB

Status: Multi-Page Dash App Plugin

The pages functionality is now part of dash 2.5! These docs remain here for legacy purposes. pages is a plugin to simplify creating multi-page Dash apps.

See the community announcement for details and discussion

Multi-Page Dash App Plugin

Background

The goal of this plugin is to remove as much boilerplate as possible when creating multi-page Dash apps.

This plugin allows users to simply place their layouts in pages/ and call dash.register_page with the desired URL path of that page.

This plugin will automatically:

  • Create the URL routing callback
  • Add page information to dash.page_registry that can be used when creating navigation bars
  • Set validate_layout accordingly so that you don't need to suppress_callback_exceptions for simple multi-page layouts
  • Set the order of dash.page_registry based off order and the filename
  • Set <title> and <meta description> and their social media equivalents accordingly in the index_string of the HTML that is served on page-load
  • Set a clientside callback to update the <title> as you navigate pages with dcc.Link
  • Set the social media meta image accordingly based off of images available in assets

Usage

Quick start examples

In the folder /demos/multi_page_basics you will find three quick start apps:

These apps serve as an entry point to run the multi-page app and provides some minimal examples of the basic and advanced features available in the API.

Creating a simple Multi-Page App

Now we will step through creating a simple multi-page app. You will find this example demos/multi-page-example1/app.py

  1. In app.py, pass the plugin into Dash. Note: In the future, this will be part of dash and you won't need to do this step.
  2. In this example, we create a navigation header using the NavbarSimple component from the dash-bootstrap-components library and populate the page names and nav links by looping through the dash.page_registry dictionary. (More on page_registry in step 4)
  3. Include dl.plugins.page_container in the app.layout This is where the page content is rendered.
import dash
import dash_labs as dl
import dash_bootstrap_components as dbc


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

navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="primary",
    dark=True,
    className="mb-2",
)

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

if __name__ == "__main__":
    app.run_server(debug=True)
  1. Create a folder called pages/ and place your app layouts in files within that folder. Each file needs to:
  • Define layout. This can be a variable or function that returns a component
  • Call dl.plugins.register_page(__name__) to tell dl.plugins.pages that this page should be part of the multi-page framework

For example, here is the first page of our app:

demos/multi-page-example1/pages/heatmaps.py

from dash_labs.plugins import register_page

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

register_page(__name__, path="/")
df = px.data.medals_wide(indexed=True)

layout = html.Div(
    [
        html.P("Medals included:"),
        dcc.Checklist(
            id="heatmaps-medals",
            options=[{"label": x, "value": x} for x in df.columns],
            value=df.columns.tolist(),
        ),
        dcc.Graph(id="heatmaps-graph"),
    ]
)


@callback(Output("heatmaps-graph", "figure"), Input("heatmaps-medals", "value"))
def filter_heatmap(cols):
    fig = px.imshow(df[cols])
    return fig

The dash.page_registry is an OrderedDict with keys being the page's module name (e.g. pages.bar-charts) and values being a dictionary containing keys path, name, order, title, description, image, and layout. As you saw in the above example, This page_registry is populated from calling register_page within pages/. register_page will accept various arguments to customize aspects about the page. See the Advanced Features section below.

multi_page


Advanced Features

These features are all optional. If you don't supply values here, the framework will take a best guess and supply them for you.

Custom Meta Tags & Titles

The page's title defines what you see in your browser tab and what would appear as the website's title in search results. By default, it is derived from the filename but it can also be set with title=

dl.plugins.register_page(__name__, title='Custom page title')

Similarly, the meta description can be set with description= and the meta image can be set with image=. Both of these tags are used as the preview when sharing a URL in a social media platforms. They're also used in search engine results.

By default, Dash will look through your assets/ folder for an image that matches the page's filename or else an image called app.<image_extension> (e.g. app.png or app.jpeg) or logo.<image_extension> (all image extensions are supported).

This image URL can also be set directly with dl.plugins.register_page(image=...) e.g.

dl.plugins.register_page(__name__, image='/assets/page-preview.png')

dash.page_registry

dash.page_registry is an OrderedDict. The keys are the module as set by __name__, e.g. pages.historical_analysis. The value is a dict with the parameters passed into register_page: path name, title, description, image, order, and layout. If these parameters aren't supplied, then they are derived from the filename.

For example: pages/historical_analysis.py

dl.plugins.register_page(__name__)

print(dash.page_registry)

would print:

OrderedDict([
    ('pages.historical_analysis', {
        'module': 'pages.historical_analysis', 
        'name': 'Historical analysis', 
        'title': 'Historical analysis',        
        'order': None,
    }
])

Whereas: pages/outlook.py

dl.plugins.register_page(__name__, path='/future', name='Future outlook', order=4)

print(dash.page_registry)

would print:

OrderedDict([
    ('pages.outlook', {
        'module': 'pages.outlook', 
        'path': '/future',
        'name': 'Future outlook', 
        'title': 'Future outlook',        
        'order': 4,
    }
])

OrderedDict values can be accessed just like regular dictionaries:

>>> print(dash.page_registry['pages.historical_analysis']['name'])
'Historical analysis'

The order of the items in page_registry is based off of the optional order= parameter:

dl.plugins.register_page(__name__, order=10)

If it's not supplied, then the order is alphanumeric based off of the filename. This order is important when rendering the page menu's dynamically in a loop. The page with the path / has order=0 by default.

Redirects

Redirects can be set with the redirect_from=[...] parameter in register_page:

pages/historical_analysis.py

dl.plugins.register_page(
    __name__,
    path='/historical',
    redirect_from=['/historical-analysis', '/historical-outlook']
)

Custom 404 Pages

404 pages can display content when the URL isn't found. By default, a simple content is displayed:

html.Div([
    html.H2('404 - Page not found'),
    html.Div(html.A('Return home', href='/')
])

However, this can be customized by creating a file called not_found_404.py

Defining Multiple Pages within a Single File

You can also pass layout= directly into register_page. Here's a quick multi-page app written in a single file:

dl.plugins.register_page('historical_analysis', path='/historical-analysis', layout=html.Div(['Historical Analysis Page'])
dl.plugins.register_page('forecast', path='/forecast', layout=html.Div(['Forecast Page'])

app.layout = dbc.Container([
    dbc.NavbarSimple([
        dbc.NavItem(dbc.NavLink(page['name'], href=page['path'])),
        for page in dash.page_registry
    ]),
    
    dash.page_container
])

However, we recommend splitting out the page layouts into their own files in pages/ to keep things more organized and to keep your files from becoming too long!

Query Strings

It's possible to pass query strings from the url to a layout function. For example:

from dash import dcc, html
from dash_labs.plugins import register_page

register_page(__name__, path='/dashboard')

def layout(velocity=0, **other_unknown_query_strings):
    return html.Div([
        dcc.Input(id='velocity', value=velocity)
    ])

image

Path Variable

Another way to pass variables to the layout is to use the path_template parameter in register_page. You can define which segments of the path are variables by marking them like this: <variable_name>. The layout function then receives the <variable_name> as a keyword argument.

For example, if path_template= "/asset/<asset_id>", and the url is "/assets/a100", then the layout will receive **{"asset_id": "a100"}. Here is an example with two variables in the path:

from dash import html
from dash_labs.plugins import register_page

register_page(
    __name__,
    path_template="/asset/<asset_id>/department/<dept_id>",
)


def layout(asset_id=None, dept_id=None, **other_unknown_query_strings):
    return html.Div(f"variables from pathname:  asset_id: {asset_id} dept_id: {dept_id}")

image


Long Callbacks

Currently long_callback requires the app object. To use long callacks with pages/ it's necessary to include the @app.long_callback in the same file where the app is instantiated. You can find an example in /demos/multi_page_long_callback.


Reference

dl.plugins.register_page

def register_page(
    module,
    path=None,
    name=None,
    order=None,
    title=None,
    description=None,
    image=None,
    layout=None,
    **kwargs
):

Assigns the variables to dash.page_registry as an OrderedDict (ordered by order).

dash.page_registry is used by pages_plugin to set up the layouts as a multi-page Dash app. This includes the URL routing callbacks (using dcc.Location) and the HTML templates to include title, meta description, and the meta description image.

dash.page_registry can also be used by Dash developers to create the page navigation links or by template authors.

  • module: The module path where this page's layout is defined. Often __name__.

  • path: URL Path, e.g. / or /home-page. If not supplied, will be inferred from module, e.g. pages.weekly_analytics to /weekly-analytics

  • path_template: Add variables to a URL by marking sections with <variable_name>. The layout function then receives the <variable_name> as a keyword argument. e.g. path_template= "/asset/<asset_id>" then if pathname in browser is "/assets/a100" then layout will receive **{"asset_id":"a100"}

  • name: The name of the link. If not supplied, will be inferred from module, e.g. pages.weekly_analytics to Weekly analytics

  • order: The order of the pages in page_registry. If not supplied, then the filename is used and the page with path / has order 0

  • title: The name of the page <title>. That is, what appears in the browser title. If not supplied, will use the supplied name or will be inferred by module, e.g. pages.weekly_analytics to Weekly analytics

  • description: The <meta type="description"></meta>. If not supplied, then nothing is supplied.

  • image: The meta description image used by social media platforms. If not supplied, then it looks for the following images in assets/:

    • A page specific image: assets/<title>.<extension> is used, e.g. assets/weekly_analytics.png
    • A generic app image at assets/app.<extension>
    • A logo at assets/logo.<extension>
  • redirect_from: A list of paths that should redirect to this page. For example: redirect_from=['/v2', '/v3']

  • layout: The layout function or component for this page. If not supplied, then looks for layout from within the supplied module.

  • **kwargs: Arbitrary keyword arguments that can be stored.

page_registry stores the original property that was passed in under supplied_<property> and the coerced property under <property>. For example, if this was called:

register_page(
    'pages.historical_outlook',
    name='Our historical view',
    custom_key='custom value'
)

Then this will appear in page_registry:

OrderedDict([
    (
        'pages.historical_outlook', 
        dict(
            module='pages.historical_outlook',
            
            supplied_path=None,
            path='/historical-outlook',
            
            supplied_name='Our historical view',
            name='Our historical view',
            
            supplied_title=None,
            title='Our historical view',
            
            supplied_description=None,
            description='Our historical view',
            
            supplied_order=None,
            order=1,
            
            supplied_layout=None,
            layout=`<function pages.historical_outlook.layout>`,
            
            custom_key='custom value'
        )
    ),
])