Embed multipage Dash app in existing Flask app with dash-bootstrap-components

Hello,

I am new to Dash and also in a beginner level to Python, so this question might be a simple Python concept question and I would be happy to get a little help here.

I try to combine the method from the following example (1) to set up a Flask application and embed a Dash app, in the form of a multipage app that follows the example of a sidebar-with-submenus (2) with the package dash-bootstrap-components.

(1)

(2)

Error description:

I think the problem is that I try to call a function in a for loop that needs the dashapp1 variable which is defined in another function and is not globally available.

It is this line: dashapp1.callback(…) in init.py

I get this traceback:

* #### File "/Users/irre/dev/dash_lab/dashapp.py", line *1* , in `<module>`

from app import create_app

* #### File "/Users/irre/dev/dash_lab/app/__init__.py", line *65* , in `<module>`

dashapp1.callback(

> NameError: name 'dashapp1' is not defined

I don’t know how to combine these two concepts properly and especially where to put the following functions in this factory application style. I guess they belong to the layout section, while still calling a callback with the dashapp1 (app) variable, which is not available outside of the def register_dashapps(app): function.

# this function is used to toggle the is_open property of each Collapse
def toggle_collapse(n, is_open):
    ...
    return is_open


# this function applies the "open" class to rotate the chevron
def set_navitem_class(is_open):
    ...
    return ""


for i in [1, 2]:
    dashapp1.callback(
    ...
    )(toggle_collapse)

    dashapp1.callback(
    ...
    )(set_navitem_class)

So, I try to post the in my opinion relevant code parts.

File structure using application factory pattern:

── app
β”‚ β”œβ”€β”€ init.py # app factory / see code example (1)
β”‚ β”œβ”€β”€ dashapp1
β”‚ β”‚ β”œβ”€β”€ assets # sidebar .css from the bootstrap example
β”‚ β”‚ β”œβ”€β”€ callbacks.py # / see code example (2)
β”‚ β”‚ β”œβ”€β”€ layout.py # / see code example (3)
β”‚ β”œβ”€β”€ extensions.py # essential flask extensions
β”‚ β”œβ”€β”€ forms.py # login & registration form
β”‚ β”œβ”€β”€ models.py # database model for users
β”‚ β”œβ”€β”€ templates # html templates
β”‚ β”‚ β”œβ”€β”€ …
β”‚ └── webapp.py # main Blueprint and Flask index, login, register routes
β”œβ”€β”€ dashapp.py # creates the server Flask app

(1) init.py

import dash
from flask import Flask
from flask.helpers import get_root_path
from flask_login import login_required

import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

from config import BaseConfig

# link fontawesome to get the chevron icons
FA = "https://use.fontawesome.com/releases/v5.8.1/css/all.css"


def create_app():
    server = Flask(__name__)
    server.config.from_object(BaseConfig)

    register_dashapps(server)
    register_extensions(server)
    register_blueprints(server)

    return server


def register_dashapps(app):
    from app.dashapp1.layout import layout
    from app.dashapp1.callbacks import register_callbacks

    # Meta tags for viewport responsiveness
    meta_viewport = {"name": "viewport", "content": "width=device-width, initial-scale=1, shrink-to-fit=no"}

    dashapp1 = dash.Dash(__name__,
                         server=app,
                         url_base_pathname='/dashboard/',
                         assets_folder=get_root_path(__name__) + '/dashboard/assets/',
                         meta_tags=[meta_viewport],
                         external_stylesheets=[dbc.themes.BOOTSTRAP, FA])

    with app.app_context():
        dashapp1.title = 'Dashapp 1'
        dashapp1.layout = layout
        register_callbacks(dashapp1)
    
    _protect_dashviews(dashapp1)


# this function is used to toggle the is_open property of each Collapse
def toggle_collapse(n, is_open):
    if n:
        return not is_open
    return is_open


# this function applies the "open" class to rotate the chevron
def set_navitem_class(is_open):
    if is_open:
        return "open"
    return ""


for i in [1, 2]:
    dashapp1.callback(
        Output(f"submenu-{i}-collapse", "is_open"),
        [Input(f"submenu-{i}", "n_clicks")],
        [State(f"submenu-{i}-collapse", "is_open")],
    )(toggle_collapse)

    dashapp1.callback(
        Output(f"submenu-{i}", "className"),
        [Input(f"submenu-{i}-collapse", "is_open")],
    )(set_navitem_class)
    

def _protect_dashviews(dashapp):
    for view_func in dashapp.server.view_functions:
        if view_func.startswith(dashapp.config.url_base_pathname):
            dashapp.server.view_functions[view_func] = login_required(dashapp.server.view_functions[view_func])


def register_extensions(server):
    from app.extensions import db
    from app.extensions import login
    from app.extensions import migrate

    db.init_app(server)
    login.init_app(server)
    login.login_view = 'main.login'
    migrate.init_app(server, db)


def register_blueprints(server):
    from app.webapp import server_bp

    server.register_blueprint(server_bp)

(2) Callback.py

from datetime import datetime as dt

import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

import pandas_datareader as pdr


def register_callbacks(dashapp):
    @dashapp.callback(Output("page-content", "children"), [Input("url", "pathname")])
    def render_page_content(pathname):
        if pathname in ["/", "/page-1/1"]:
            return html.P("This is the content of page 1.1!")
        elif pathname == "/page-1/2":
            return html.P("This is the content of page 1.2. Yay!")
        elif pathname == "/page-2/1":
            return html.P("Oh cool, this is page 2.1!")
        elif pathname == "/page-2/2":
            return html.P("No way! This is page 2.2!")
        # If the user tries to reach a different page, return a 404 message
        return dbc.Jumbotron(
            [
                html.H1("404: Not found", className="text-danger"),
                html.Hr(),
                html.P(f"The pathname {pathname} was not recognised..."),
            ]
        )

(3) layout.py

import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

# link fontawesome to get the chevron icons
FA = "https://use.fontawesome.com/releases/v5.8.1/css/all.css"


# the style arguments for the sidebar. We use position:fixed and a fixed width
SIDEBAR_STYLE = {
    "position": "fixed",
    "top": 0,
    "left": 0,
    "bottom": 0,
    "width": "16rem",
    "padding": "2rem 1rem",
    "background-color": "#f8f9fa",
}

# the styles for the main content position it to the right of the sidebar and
# add some padding.
CONTENT_STYLE = {
    "margin-left": "18rem",
    "margin-right": "2rem",
    "padding": "2rem 1rem",
}

submenu_1 = [
    html.Li(
        # use Row and Col components to position the chevrons
        dbc.Row(
            [
                dbc.Col("Menu 1"),
                dbc.Col(
                    html.I(className="fas fa-chevron-right mr-3"), width="auto"
                ),
            ],
            className="my-1",
        ),
        id="submenu-1",
    ),
    # we use the Collapse component to hide and reveal the navigation links
    dbc.Collapse(
        [
            dbc.NavLink("Page 1.1", href="/page-1/1"),
            dbc.NavLink("Page 1.2", href="/page-1/2"),
        ],
        id="submenu-1-collapse",
    ),
]

submenu_2 = [
    html.Li(
        dbc.Row(
            [
                dbc.Col("Menu 2"),
                dbc.Col(
                    html.I(className="fas fa-chevron-right mr-3"), width="auto"
                ),
            ],
            className="my-1",
        ),
        id="submenu-2",
    ),
    dbc.Collapse(
        [
            dbc.NavLink("Page 2.1", href="/page-2/1"),
            dbc.NavLink("Page 2.2", href="/page-2/2"),
        ],
        id="submenu-2-collapse",
    ),
]


sidebar = html.Div(
    [
        html.H2("Sidebar", className="display-4"),
        html.Hr(),
        html.P(
            "A sidebar with collapsible navigation links", className="lead"
        ),
        dbc.Nav(submenu_1 + submenu_2, vertical=True),
    ],
    style=SIDEBAR_STYLE,
    id="sidebar",
)

content = html.Div(id="page-content", style=CONTENT_STYLE)

layout = html.Div([dcc.Location(id="url"), sidebar, content]) # here I changed app.layout to just layout

thanks for your help!

@okomarov
@tcbegley

I think you can probably fix this fairly simply by just moving toggle_collapse, set_navitem_class and the for loop with dashapp1.callback into the register_callbacks function in app/dashapp1/callbacks.py. You’ll need to change dashapp1.callback to just dashapp.callback. You want to use the callback method of you actual app object (which in your case is dashapp).

Give that a try anyway and let us know if it works.

Thanks a lot, it’s working now!

Only the chevron doesn’t indicate down, when the submenu is opened yet.

Also had to add my dashboard route, so a reload of the page doesn’t lead to an error 404.

here is the callback.py code now:

from datetime import datetime as dt

import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

import pandas_datareader as pdr


def register_callbacks(dashapp):
    @dashapp.callback(Output("page-content", "children"), [Input("url", "pathname")])
    def render_page_content(pathname):
        if pathname in ["/", "/dashboard/page-1/1"]:
            return html.P("This is the content of page 1.1!")
        elif pathname == "/dashboard/page-1/2":
            return html.P("This is the content of page 1.2. Yay!")
        elif pathname == "/dashboard/page-2/1":
            return html.P("Oh cool, this is page 2.1!")
        elif pathname == "/dashboard/page-2/2":
            return html.P("No way! This is page 2.2!")
        # If the user tries to reach a different page, return a 404 message
        return dbc.Jumbotron(
            [
                html.H1("404: Not found", className="text-danger"),
                html.Hr(),
                html.P(f"The pathname {pathname} was not recognised..."),
            ]
        )
              
    # this function is used to toggle the is_open property of each Collapse
    def toggle_collapse(n, is_open):
        if n:
            return not is_open
        return is_open

    # this function applies the "open" class to rotate the chevron
    def set_navitem_class(is_open):
        if is_open:
            return "open"
        return ""

    for i in [1, 2]:
        dashapp.callback(
            Output(f"submenu-{i}-collapse", "is_open"),
            [Input(f"submenu-{i}", "n_clicks")],
            [State(f"submenu-{i}-collapse", "is_open")],
        )(toggle_collapse)

        dashapp.callback(
            Output(f"submenu-{i}", "className"),
            [Input(f"submenu-{i}-collapse", "is_open")],
        )(set_navitem_class)

Thanks again for your help!

Did you include the CSS from the sidebar example? You’ll probably want to check carefully that it’s getting included, looking here it seems that dash_on_flask reconfigures the location of the assets/ folder which might be causing you problems.

Edit: you may be right yes, I thought with the look from below the css is loading, but that’s just the template I think.

This is the look with the following code:

  dashapp1 = dash.Dash(__name__,
                         server=app,
                         url_base_pathname='/dashboard/',
                         assets_folder=get_root_path(__name__) + '/dashboard/assets/',
                         meta_tags=[meta_viewport],
                         external_stylesheets=[dbc.themes.BOOTSTRAP, FA])

Bildschirmfoto 2020-02-11 um 16.42.49

and this without the external stylesheet:

dashapp1 = dash.Dash(__name__,
                         server=app,
                         url_base_pathname='/dashboard/',
                         assets_folder=get_root_path(__name__) + '/dashboard/assets/',
                         meta_tags=[meta_viewport])

Bildschirmfoto 2020-02-11 um 16.42.18

Now it’s working! Yes, it was a wrong path in that asset_folder instruction.

dashapp1 = dash.Dash(__name__,
                         server=app,
                         url_base_pathname='/dashboard/',
                         assets_folder=get_root_path(__name__) + '/dashapp1/assets/',
                         meta_tags=[meta_viewport],
                         external_stylesheets=[dbc.themes.BOOTSTRAP, FA])

Bildschirmfoto 2020-02-11 um 17.22.04

So happy! Thanks again for the help :slight_smile:

2 Likes